1.12.3 #60

Merged
defnotken merged 91 commits from 1.12.3 into master 2025-10-24 14:22:21 +00:00
10 changed files with 1274 additions and 88 deletions
Showing only changes of commit 0cb71e5444 - Show all commits

1086
CONTRIBUTING.md Normal file

File diff suppressed because it is too large Load Diff

111
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,111 @@
# Development Setup for macOS
This document explains how to set up the Lightless Sync development environment on macOS.
## Problem: "Cannot resolve symbol 'Dalamud'"
When developing Dalamud plugins on macOS, you may encounter the error:
```
Cannot resolve symbol 'Dalamud'
Dalamud.NET.Sdk: Dalamud installation not found at /Users/.../Library/Application Support/XIV on Mac/dalamud/Hooks/dev/
```
This happens because the Dalamud.NET.Sdk expects to find Dalamud assemblies at a specific path, but they don't exist if you don't have XIV on Mac or Dalamud installed.
## Solution
### Automated Setup (Recommended)
Run the setup script to download the required Dalamud assemblies:
```bash
./setup-dalamud.sh
```
This script will:
1. Create a development directory at `~/.dalamud/dev`
2. Download the latest Dalamud assemblies from the official distribution
3. Extract them to the development directory
### Manual Setup
If you prefer to set up manually:
1. **Create the Dalamud directory:**
```bash
mkdir -p ~/.dalamud/dev
```
2. **Download Dalamud assemblies:**
```bash
curl -L -o /tmp/dalamud.zip https://goatcorp.github.io/dalamud-distrib/latest.zip
unzip /tmp/dalamud.zip -d ~/.dalamud/dev
```
3. **Set the DALAMUD_HOME environment variable (optional):**
```bash
export DALAMUD_HOME="$HOME/.dalamud/dev"
```
## How It Works
The project includes a `Directory.Build.props` file that automatically configures the `DALAMUD_HOME` path to use `~/.dalamud/dev` if it exists. This overrides the default XIV on Mac path.
The Dalamud.NET.Sdk will then use this path to find the required assemblies for compilation and IntelliSense.
## Building the Project
After setup, you can build the project normally:
```bash
dotnet restore
dotnet build
```
## IDE Configuration
### JetBrains Rider / IntelliJ IDEA
After running the setup script, you may need to:
1. Invalidate caches and restart: **File → Invalidate Caches → Invalidate and Restart**
2. Reload the solution: **Right-click on solution → Reload All Projects**
The IDE should now resolve all Dalamud symbols correctly.
## Troubleshooting
### Build still fails with "Dalamud installation not found"
1. Verify the assemblies were downloaded:
```bash
ls -la ~/.dalamud/dev/Dalamud.dll
```
2. Check that `Directory.Build.props` exists in the project root
3. Try cleaning and rebuilding:
```bash
dotnet clean
dotnet build
```
### IDE still shows "Cannot resolve symbol 'Dalamud'"
1. Ensure the build succeeds first (run `dotnet build`)
2. Restart your IDE
3. Try invalidating caches (Rider/IntelliJ)
4. Check that the project references are loaded correctly
## Files Modified
- `Directory.Build.props` - Configures DALAMUD_HOME path
- `LightlessSync/LightlessSync.csproj` - Removed duplicate DalamudPackager reference
- `PenumbraAPI/Penumbra.Api.csproj` - Added DalamudLibPath configuration
- `setup-dalamud.sh` - Setup script to download Dalamud assemblies
## Additional Notes
- The Dalamud assemblies are only needed for development/compilation
- You don't need a running FFXIV or XIV on Mac installation to develop plugins
- The assemblies are downloaded from the official Dalamud distribution
- Updates to Dalamud may require re-running the setup script

View File

@@ -269,8 +269,7 @@ public sealed class Plugin : IDalamudPlugin
s.GetRequiredService<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(),
s.GetRequiredService<UiFactory>(),
s.GetRequiredService<FileDialogManager>(),
s.GetRequiredService<LightlessMediator>(),
s.GetRequiredService<NotificationService>()));
s.GetRequiredService<LightlessMediator>()));
collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(),
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(),
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessConfigService>()));

View File

@@ -108,7 +108,9 @@ public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
public record PairRequestReceivedMessage(string HashedCid, string Message) : MessageBase;
public record PairRequestsUpdatedMessage : MessageBase;
public record PairDownloadStatusMessage(List<(string PlayerName, float Progress, string Status)> DownloadStatus, int QueueWaiting) : MessageBase;
public record VisibilityChange : MessageBase;
#pragma warning restore S2094
#pragma warning restore MA0048 // File name must match type name

View File

@@ -45,7 +45,9 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
public Task StartAsync(CancellationToken cancellationToken)
{
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
Mediator.Subscribe<PairRequestReceivedMessage>(this, HandlePairRequestReceived);
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
Mediator.Subscribe<PairDownloadStatusMessage>(this, HandlePairDownloadStatus);
Mediator.Subscribe<PerformanceNotificationMessage>(this, HandlePerformanceNotification);
return Task.CompletedTask;
}
@@ -293,33 +295,8 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
return actions;
}
public void ShowPairDownloadNotification(List<(string playerName, float progress, string status)> downloadStatus,
int queueWaiting = 0)
{
var userDownloads = downloadStatus.Where(x => x.playerName != "Pair Queue").ToList();
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.progress) : 0f;
var message = BuildPairDownloadMessage(userDownloads, queueWaiting);
var notification = new LightlessNotification
{
Id = "pair_download_progress",
Title = "Downloading Pair Data",
Message = message,
Type = NotificationType.Download,
Duration = TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
ShowProgress = true,
Progress = totalProgress
};
Mediator.Publish(new LightlessNotificationMessage(notification));
if (AreAllDownloadsCompleted(userDownloads))
{
DismissPairDownloadNotification();
}
}
private string BuildPairDownloadMessage(List<(string playerName, float progress, string status)> userDownloads,
private string BuildPairDownloadMessage(List<(string PlayerName, float Progress, string Status)> userDownloads,
int queueWaiting)
{
var messageParts = new List<string>();
@@ -331,7 +308,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
if (userDownloads.Count > 0)
{
var completedCount = userDownloads.Count(x => x.progress >= 1.0f);
var completedCount = userDownloads.Count(x => x.Progress >= 1.0f);
messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed");
}
@@ -344,29 +321,29 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
return string.Join("\n", messageParts);
}
private string BuildActiveDownloadLines(List<(string playerName, float progress, string status)> userDownloads)
private string BuildActiveDownloadLines(List<(string PlayerName, float Progress, string Status)> userDownloads)
{
var activeDownloads = userDownloads
.Where(x => x.progress < 1.0f)
.Where(x => x.Progress < 1.0f)
.Take(_configService.Current.MaxConcurrentPairApplications);
if (!activeDownloads.Any()) return string.Empty;
return string.Join("\n", activeDownloads.Select(x => $"• {x.playerName}: {FormatDownloadStatus(x)}"));
return string.Join("\n", activeDownloads.Select(x => $"• {x.PlayerName}: {FormatDownloadStatus(x)}"));
}
private string FormatDownloadStatus((string playerName, float progress, string status) download) =>
download.status switch
private string FormatDownloadStatus((string PlayerName, float Progress, string Status) download) =>
download.Status switch
{
"downloading" => $"{download.progress:P0}",
"downloading" => $"{download.Progress:P0}",
"decompressing" => "decompressing",
"queued" => "queued",
"waiting" => "waiting for slot",
_ => download.status
_ => download.Status
};
private bool AreAllDownloadsCompleted(List<(string playerName, float progress, string status)> userDownloads) =>
userDownloads.Any() && userDownloads.All(x => x.progress >= 1.0f);
private bool AreAllDownloadsCompleted(List<(string PlayerName, float Progress, string Status)> userDownloads) =>
userDownloads.Any() && userDownloads.All(x => x.Progress >= 1.0f);
public void DismissPairDownloadNotification() =>
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
@@ -581,12 +558,25 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
_chatGui.Print(se.BuiltString);
}
private void HandlePairRequestReceived(PairRequestReceivedMessage msg)
{
var request = _pairRequestService.RegisterIncomingRequest(msg.HashedCid, msg.Message);
var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName;
_shownPairRequestNotifications.Add(request.HashedCid);
ShowPairRequestNotification(
senderName,
request.HashedCid,
onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid, senderName),
onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid));
}
private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _)
{
var activeRequests = _pairRequestService.GetActiveRequests();
var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet();
// Dismiss notifications for requests that are no longer active
// Dismiss notifications for requests that are no longer active (expired)
var notificationsToRemove = _shownPairRequestNotifications
.Where(hashedCid => !activeRequestIds.Contains(hashedCid))
.ToList();
@@ -597,17 +587,30 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
Mediator.Publish(new LightlessNotificationDismissMessage(notificationId));
_shownPairRequestNotifications.Remove(hashedCid);
}
}
// Show/update notifications for all active requests
foreach (var request in activeRequests)
private void HandlePairDownloadStatus(PairDownloadStatusMessage msg)
{
var userDownloads = msg.DownloadStatus.Where(x => x.PlayerName != "Pair Queue").ToList();
var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.Progress) : 0f;
var message = BuildPairDownloadMessage(userDownloads, msg.QueueWaiting);
var notification = new LightlessNotification
{
_shownPairRequestNotifications.Add(request.HashedCid);
ShowPairRequestNotification(
request.DisplayName,
request.HashedCid,
() => _pairRequestService.AcceptPairRequest(request.HashedCid, request.DisplayName),
() => _pairRequestService.DeclinePairRequest(request.HashedCid)
);
Id = "pair_download_progress",
Title = "Downloading Pair Data",
Message = message,
Type = NotificationType.Download,
Duration = TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
ShowProgress = true,
Progress = totalProgress
};
Mediator.Publish(new LightlessNotificationMessage(notification));
if (AreAllDownloadsCompleted(userDownloads))
{
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
}
}

View File

@@ -19,7 +19,12 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase
private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5);
public PairRequestService(ILogger<PairRequestService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager, Lazy<WebAPI.ApiController> apiController)
public PairRequestService(
ILogger<PairRequestService> logger,
LightlessMediator mediator,
DalamudUtilService dalamudUtil,
PairManager pairManager,
Lazy<WebAPI.ApiController> apiController)
: base(logger, mediator)
{
_dalamudUtil = dalamudUtil;

View File

@@ -23,8 +23,7 @@ public sealed class UiService : DisposableMediatorSubscriberBase
LightlessConfigService lightlessConfigService, WindowSystem windowSystem,
IEnumerable<WindowMediatorSubscriberBase> windows,
UiFactory uiFactory, FileDialogManager fileDialogManager,
LightlessMediator lightlessMediator,
NotificationService notificationService) : base(logger, lightlessMediator)
LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
{
_logger = logger;
_logger.LogTrace("Creating {type}", GetType().Name);

View File

@@ -22,13 +22,12 @@ public class DownloadUi : WindowMediatorSubscriberBase
private readonly UiSharedService _uiShared;
private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
private readonly NotificationService _notificationService;
private bool _notificationDismissed = true;
private int _lastDownloadStateHash = 0;
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared,
PerformanceCollectorService performanceCollectorService, NotificationService notificationService)
PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
{
_dalamudUtilService = dalamudUtilService;
@@ -36,7 +35,6 @@ public class DownloadUi : WindowMediatorSubscriberBase
_pairProcessingLimiter = pairProcessingLimiter;
_fileTransferManager = fileTransferManager;
_uiShared = uiShared;
_notificationService = notificationService;
SizeConstraints = new WindowSizeConstraints()
{
@@ -359,7 +357,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
_lastDownloadStateHash = currentHash;
if (downloadStatus.Count > 0 || queueWaiting > 0)
{
_notificationService.ShowPairDownloadNotification(downloadStatus, queueWaiting);
Mediator.Publish(new PairDownloadStatusMessage(downloadStatus, queueWaiting));
}
}
}

View File

@@ -63,7 +63,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
private readonly NameplateService _nameplateService;
private readonly NameplateHandler _nameplateHandler;
private readonly NotificationService _lightlessNotificationService;
private (int, int, FileCacheEntity) _currentProgress;
private bool _deleteAccountPopupModalShown = false;
private bool _deleteFilesPopupModalShown = false;
@@ -107,8 +106,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
IpcManager ipcManager, CacheMonitor cacheMonitor,
DalamudUtilService dalamudUtilService, HttpClient httpClient,
NameplateService nameplateService,
NameplateHandler nameplateHandler,
NotificationService lightlessNotificationService) : base(logger, mediator, "Lightless Sync Settings",
NameplateHandler nameplateHandler) : base(logger, mediator, "Lightless Sync Settings",
performanceCollector)
{
_configService = configService;
@@ -130,7 +128,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared = uiShared;
_nameplateService = nameplateService;
_nameplateHandler = nameplateHandler;
_lightlessNotificationService = lightlessNotificationService;
AllowClickthrough = false;
AllowPinning = true;
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
@@ -3616,20 +3613,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_pair", new Vector2(availableWidth, 0)))
{
_lightlessNotificationService.ShowPairRequestNotification(
"Test User",
"test-uid-123",
() =>
{
Mediator.Publish(new NotificationMessage("Accepted", "You accepted the test pair request.",
NotificationType.Info));
},
() =>
{
Mediator.Publish(new NotificationMessage("Declined", "You declined the test pair request.",
NotificationType.Info));
}
);
Mediator.Publish(new PairRequestReceivedMessage("test-uid-123", "Test User wants to pair with you."));
}
}
UiSharedService.AttachToolTip("Test pair request notification");
@@ -3652,15 +3636,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
{
if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_download", new Vector2(availableWidth, 0)))
{
_lightlessNotificationService.ShowPairDownloadNotification(
new List<(string playerName, float progress, string status)>
{
Mediator.Publish(new PairDownloadStatusMessage(
[
("Player One", 0.35f, "downloading"),
("Player Two", 0.75f, "downloading"),
("Player Three", 1.0f, "downloading")
},
queueWaiting: 2
);
],
2
));
}
}
UiSharedService.AttachToolTip("Test download progress notification");

View File

@@ -107,17 +107,17 @@ public partial class ApiController
}
public Task Client_ReceiveBroadcastPairRequest(UserPairNotificationDto dto)
{
if (dto == null)
Logger.LogDebug("Client_ReceiveBroadcastPairRequest: {dto}", dto);
if (dto is null)
{
return Task.CompletedTask;
}
var request = _pairRequestService.RegisterIncomingRequest(dto.myHashedCid, dto.message ?? string.Empty);
var senderName = string.IsNullOrEmpty(request.DisplayName) ? "Unknown User" : request.DisplayName;
_lightlessNotificationService.ShowPairRequestNotification(
senderName,
request.HashedCid,
onAccept: () => _pairRequestService.AcceptPairRequest(request.HashedCid, senderName),
onDecline: () => _pairRequestService.DeclinePairRequest(request.HashedCid));
ExecuteSafely(() =>
{
Mediator.Publish(new PairRequestReceivedMessage(dto.myHashedCid, dto.message ?? string.Empty));
});
return Task.CompletedTask;
}