service cleanups, containing logic directly now

This commit is contained in:
choco
2025-10-20 14:00:54 +02:00
parent cac94374d9
commit 0cb71e5444
10 changed files with 1274 additions and 88 deletions

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<WindowSystem>(), s.GetServices<WindowMediatorSubscriberBase>(),
s.GetRequiredService<UiFactory>(), s.GetRequiredService<UiFactory>(),
s.GetRequiredService<FileDialogManager>(), s.GetRequiredService<FileDialogManager>(),
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessMediator>()));
s.GetRequiredService<NotificationService>()));
collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(), collection.AddScoped((s) => new CommandManagerService(commandManager, s.GetRequiredService<PerformanceCollectorService>(),
s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<CacheMonitor>(), s.GetRequiredService<ApiController>(),
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<LightlessConfigService>())); 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 EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase; public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
public record SyncshellBroadcastsUpdatedMessage : MessageBase; public record SyncshellBroadcastsUpdatedMessage : MessageBase;
public record PairRequestReceivedMessage(string HashedCid, string Message) : MessageBase;
public record PairRequestsUpdatedMessage : MessageBase; public record PairRequestsUpdatedMessage : MessageBase;
public record PairDownloadStatusMessage(List<(string PlayerName, float Progress, string Status)> DownloadStatus, int QueueWaiting) : MessageBase;
public record VisibilityChange : MessageBase; public record VisibilityChange : MessageBase;
#pragma warning restore S2094 #pragma warning restore S2094
#pragma warning restore MA0048 // File name must match type name #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) public Task StartAsync(CancellationToken cancellationToken)
{ {
Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage); Mediator.Subscribe<NotificationMessage>(this, HandleNotificationMessage);
Mediator.Subscribe<PairRequestReceivedMessage>(this, HandlePairRequestReceived);
Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated); Mediator.Subscribe<PairRequestsUpdatedMessage>(this, HandlePairRequestsUpdated);
Mediator.Subscribe<PairDownloadStatusMessage>(this, HandlePairDownloadStatus);
Mediator.Subscribe<PerformanceNotificationMessage>(this, HandlePerformanceNotification); Mediator.Subscribe<PerformanceNotificationMessage>(this, HandlePerformanceNotification);
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -293,33 +295,8 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
return actions; 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 private string BuildPairDownloadMessage(List<(string PlayerName, float Progress, string Status)> userDownloads,
{
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,
int queueWaiting) int queueWaiting)
{ {
var messageParts = new List<string>(); var messageParts = new List<string>();
@@ -331,7 +308,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
if (userDownloads.Count > 0) 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"); messageParts.Add($"Progress: {completedCount}/{userDownloads.Count} completed");
} }
@@ -344,29 +321,29 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
return string.Join("\n", messageParts); 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 var activeDownloads = userDownloads
.Where(x => x.progress < 1.0f) .Where(x => x.Progress < 1.0f)
.Take(_configService.Current.MaxConcurrentPairApplications); .Take(_configService.Current.MaxConcurrentPairApplications);
if (!activeDownloads.Any()) return string.Empty; 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) => private string FormatDownloadStatus((string PlayerName, float Progress, string Status) download) =>
download.status switch download.Status switch
{ {
"downloading" => $"{download.progress:P0}", "downloading" => $"{download.Progress:P0}",
"decompressing" => "decompressing", "decompressing" => "decompressing",
"queued" => "queued", "queued" => "queued",
"waiting" => "waiting for slot", "waiting" => "waiting for slot",
_ => download.status _ => download.Status
}; };
private bool AreAllDownloadsCompleted(List<(string playerName, float progress, string status)> userDownloads) => private bool AreAllDownloadsCompleted(List<(string PlayerName, float Progress, string Status)> userDownloads) =>
userDownloads.Any() && userDownloads.All(x => x.progress >= 1.0f); userDownloads.Any() && userDownloads.All(x => x.Progress >= 1.0f);
public void DismissPairDownloadNotification() => public void DismissPairDownloadNotification() =>
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress")); Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
@@ -581,12 +558,25 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
_chatGui.Print(se.BuiltString); _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 _) private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _)
{ {
var activeRequests = _pairRequestService.GetActiveRequests(); var activeRequests = _pairRequestService.GetActiveRequests();
var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet(); 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 var notificationsToRemove = _shownPairRequestNotifications
.Where(hashedCid => !activeRequestIds.Contains(hashedCid)) .Where(hashedCid => !activeRequestIds.Contains(hashedCid))
.ToList(); .ToList();
@@ -597,17 +587,30 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
Mediator.Publish(new LightlessNotificationDismissMessage(notificationId)); Mediator.Publish(new LightlessNotificationDismissMessage(notificationId));
_shownPairRequestNotifications.Remove(hashedCid); _shownPairRequestNotifications.Remove(hashedCid);
} }
}
// Show/update notifications for all active requests private void HandlePairDownloadStatus(PairDownloadStatusMessage msg)
foreach (var request in activeRequests) {
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); Id = "pair_download_progress",
ShowPairRequestNotification( Title = "Downloading Pair Data",
request.DisplayName, Message = message,
request.HashedCid, Type = NotificationType.Download,
() => _pairRequestService.AcceptPairRequest(request.HashedCid, request.DisplayName), Duration = TimeSpan.FromSeconds(_configService.Current.DownloadNotificationDurationSeconds),
() => _pairRequestService.DeclinePairRequest(request.HashedCid) 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); 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) : base(logger, mediator)
{ {
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;

View File

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

View File

@@ -22,13 +22,12 @@ public class DownloadUi : WindowMediatorSubscriberBase
private readonly UiSharedService _uiShared; private readonly UiSharedService _uiShared;
private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new(); private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
private readonly NotificationService _notificationService;
private bool _notificationDismissed = true; private bool _notificationDismissed = true;
private int _lastDownloadStateHash = 0; private int _lastDownloadStateHash = 0;
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService, public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared, PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared,
PerformanceCollectorService performanceCollectorService, NotificationService notificationService) PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService) : base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
{ {
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
@@ -36,7 +35,6 @@ public class DownloadUi : WindowMediatorSubscriberBase
_pairProcessingLimiter = pairProcessingLimiter; _pairProcessingLimiter = pairProcessingLimiter;
_fileTransferManager = fileTransferManager; _fileTransferManager = fileTransferManager;
_uiShared = uiShared; _uiShared = uiShared;
_notificationService = notificationService;
SizeConstraints = new WindowSizeConstraints() SizeConstraints = new WindowSizeConstraints()
{ {
@@ -359,7 +357,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
_lastDownloadStateHash = currentHash; _lastDownloadStateHash = currentHash;
if (downloadStatus.Count > 0 || queueWaiting > 0) 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 IProgress<(int, int, FileCacheEntity)> _validationProgress;
private readonly NameplateService _nameplateService; private readonly NameplateService _nameplateService;
private readonly NameplateHandler _nameplateHandler; private readonly NameplateHandler _nameplateHandler;
private readonly NotificationService _lightlessNotificationService;
private (int, int, FileCacheEntity) _currentProgress; private (int, int, FileCacheEntity) _currentProgress;
private bool _deleteAccountPopupModalShown = false; private bool _deleteAccountPopupModalShown = false;
private bool _deleteFilesPopupModalShown = false; private bool _deleteFilesPopupModalShown = false;
@@ -107,8 +106,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
IpcManager ipcManager, CacheMonitor cacheMonitor, IpcManager ipcManager, CacheMonitor cacheMonitor,
DalamudUtilService dalamudUtilService, HttpClient httpClient, DalamudUtilService dalamudUtilService, HttpClient httpClient,
NameplateService nameplateService, NameplateService nameplateService,
NameplateHandler nameplateHandler, NameplateHandler nameplateHandler) : base(logger, mediator, "Lightless Sync Settings",
NotificationService lightlessNotificationService) : base(logger, mediator, "Lightless Sync Settings",
performanceCollector) performanceCollector)
{ {
_configService = configService; _configService = configService;
@@ -130,7 +128,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared = uiShared; _uiShared = uiShared;
_nameplateService = nameplateService; _nameplateService = nameplateService;
_nameplateHandler = nameplateHandler; _nameplateHandler = nameplateHandler;
_lightlessNotificationService = lightlessNotificationService;
AllowClickthrough = false; AllowClickthrough = false;
AllowPinning = true; AllowPinning = true;
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v); _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))) if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_pair", new Vector2(availableWidth, 0)))
{ {
_lightlessNotificationService.ShowPairRequestNotification( Mediator.Publish(new PairRequestReceivedMessage("test-uid-123", "Test User wants to pair with you."));
"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));
}
);
} }
} }
UiSharedService.AttachToolTip("Test pair request notification"); 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))) if (ImGui.Button($"{FontAwesomeIcon.Play.ToIconString()}##test_download", new Vector2(availableWidth, 0)))
{ {
_lightlessNotificationService.ShowPairDownloadNotification( Mediator.Publish(new PairDownloadStatusMessage(
new List<(string playerName, float progress, string status)> [
{
("Player One", 0.35f, "downloading"), ("Player One", 0.35f, "downloading"),
("Player Two", 0.75f, "downloading"), ("Player Two", 0.75f, "downloading"),
("Player Three", 1.0f, "downloading") ("Player Three", 1.0f, "downloading")
}, ],
queueWaiting: 2 2
); ));
} }
} }
UiSharedService.AttachToolTip("Test download progress notification"); UiSharedService.AttachToolTip("Test download progress notification");

View File

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