From c5e6c060055bace94f961d18e874859edfda06ef Mon Sep 17 00:00:00 2001 From: defnotken Date: Tue, 16 Sep 2025 04:14:32 +0200 Subject: [PATCH] 1.11.10 (#31) Co-authored-by: CakeAndBanana Co-authored-by: defnotken Reviewed-on: https://git.lightless-sync.org/Lightless-Sync/LightlessClient/pulls/31 --- LightlessSync/FileCache/CacheMonitor.cs | 4 +- LightlessSync/FileCache/FileCacheManager.cs | 82 ++++++++++++------- .../Configurations/LightlessConfig.cs | 1 + .../Configurations/PlayerPerformanceConfig.cs | 2 + LightlessSync/LightlessSync.csproj | 2 +- .../PlayerData/Handlers/PairHandler.cs | 28 +++++-- .../Services/CharaData/CharaDataManager.cs | 6 +- .../CharaData/CharaDataNearbyManager.cs | 4 +- LightlessSync/Services/DalamudUtilService.cs | 43 ++++++---- LightlessSync/Services/Mediator/Messages.cs | 6 +- LightlessSync/UI/CompactUI.cs | 24 ++++-- LightlessSync/UI/SettingsUi.cs | 55 ++++++++----- LightlessSync/UI/SyncshellAdminUI.cs | 31 +++++-- .../WebAPI/Files/FileDownloadManager.cs | 68 +++++++++++---- .../WebAPI/Files/FileUploadManager.cs | 4 +- 15 files changed, 246 insertions(+), 114 deletions(-) diff --git a/LightlessSync/FileCache/CacheMonitor.cs b/LightlessSync/FileCache/CacheMonitor.cs index 79d225b..b03f311 100644 --- a/LightlessSync/FileCache/CacheMonitor.cs +++ b/LightlessSync/FileCache/CacheMonitor.cs @@ -383,7 +383,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase scanThread.Start(); while (scanThread.IsAlive) { - await Task.Delay(250).ConfigureAwait(false); + await Task.Delay(250, token).ConfigureAwait(false); } TotalFiles = 0; _currentFileProgress = 0; @@ -619,7 +619,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase return; } - if (entitiesToUpdate.Any() || entitiesToRemove.Any()) + if (entitiesToUpdate.Count != 0 || entitiesToRemove.Count != 0) { foreach (var entity in entitiesToUpdate) { diff --git a/LightlessSync/FileCache/FileCacheManager.cs b/LightlessSync/FileCache/FileCacheManager.cs index 5c6084d..831a534 100644 --- a/LightlessSync/FileCache/FileCacheManager.cs +++ b/LightlessSync/FileCache/FileCacheManager.cs @@ -21,7 +21,7 @@ public sealed class FileCacheManager : IHostedService private readonly string _csvPath; private readonly ConcurrentDictionary> _fileCaches = new(StringComparer.Ordinal); private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1); - private readonly object _fileWriteLock = new(); + private readonly Lock _fileWriteLock = new(); private readonly IpcManager _ipcManager; private readonly ILogger _logger; public string CacheFolder => _configService.Current.CacheFolder; @@ -42,10 +42,7 @@ public sealed class FileCacheManager : IHostedService FileInfo fi = new(path); if (!fi.Exists) return null; _logger.LogTrace("Creating cache entry for {path}", path); - var fullName = fi.FullName.ToLowerInvariant(); - if (!fullName.Contains(_configService.Current.CacheFolder.ToLowerInvariant(), StringComparison.Ordinal)) return null; - string prefixedPath = fullName.Replace(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); - return CreateFileCacheEntity(fi, prefixedPath); + return CreateFileEntity(_configService.Current.CacheFolder.ToLowerInvariant(), CachePrefix, fi); } public FileCacheEntity? CreateFileEntry(string path) @@ -53,9 +50,14 @@ public sealed class FileCacheManager : IHostedService FileInfo fi = new(path); if (!fi.Exists) return null; _logger.LogTrace("Creating file entry for {path}", path); + return CreateFileEntity(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix, fi); + } + + private FileCacheEntity? CreateFileEntity(string directory, string prefix, FileInfo fi) + { var fullName = fi.FullName.ToLowerInvariant(); - if (!fullName.Contains(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), StringComparison.Ordinal)) return null; - string prefixedPath = fullName.Replace(_ipcManager.Penumbra.ModDirectory!.ToLowerInvariant(), PenumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); + if (!fullName.Contains(_configService.Current.CacheFolder.ToLowerInvariant(), StringComparison.Ordinal)) return null; + string prefixedPath = fullName.Replace(directory, prefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); return CreateFileCacheEntity(fi, prefixedPath); } @@ -66,7 +68,7 @@ public sealed class FileCacheManager : IHostedService List output = []; if (_fileCaches.TryGetValue(hash, out var fileCacheEntities)) { - foreach (var fileCache in fileCacheEntities.Where(c => ignoreCacheEntries ? !c.IsCacheEntry : true).ToList()) + foreach (var fileCache in fileCacheEntities.Where(c => !ignoreCacheEntries || !c.IsCacheEntry).ToList()) { if (!validate) output.Add(fileCache); else @@ -106,7 +108,7 @@ public sealed class FileCacheManager : IHostedService var computedHash = Crypto.GetFileHash(fileCache.ResolvedFilepath); if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal)) { - _logger.LogInformation("Failed to validate {file}, got hash {hash}, expected hash {hash}", fileCache.ResolvedFilepath, computedHash, fileCache.Hash); + _logger.LogInformation("Failed to validate {file}, got hash {computedHash}, expected hash {hash}", fileCache.ResolvedFilepath, computedHash, fileCache.Hash); brokenEntities.Add(fileCache); } } @@ -151,7 +153,7 @@ public sealed class FileCacheManager : IHostedService { if (_fileCaches.TryGetValue(hash, out var hashes)) { - var item = hashes.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix) ? 0 : 1).FirstOrDefault(); + var item = hashes.OrderBy(p => p.PrefixedFilePath.Contains(PenumbraPrefix, StringComparison.Ordinal) ? 0 : 1).FirstOrDefault(); if (item != null) return GetValidatedFileCache(item); } return null; @@ -180,48 +182,66 @@ public sealed class FileCacheManager : IHostedService try { - var cleanedPaths = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var p in paths) + var allEntities = _fileCaches.SelectMany(f => f.Value).ToArray(); + + var cacheDict = new ConcurrentDictionary( + StringComparer.OrdinalIgnoreCase); + + Parallel.ForEach(allEntities, entity => + { + cacheDict[entity.PrefixedFilePath] = entity; + }); + + var cleanedPaths = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + var seenCleaned = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + Parallel.ForEach(paths, p => { var cleaned = p.Replace("/", "\\", StringComparison.OrdinalIgnoreCase) - .Replace(_ipcManager.Penumbra.ModDirectory!, _ipcManager.Penumbra.ModDirectory!.EndsWith('\\') ? PenumbraPrefix + '\\' : PenumbraPrefix, StringComparison.OrdinalIgnoreCase) - .Replace(_configService.Current.CacheFolder, _configService.Current.CacheFolder.EndsWith('\\') ? CachePrefix + '\\' : CachePrefix, StringComparison.OrdinalIgnoreCase) + .Replace( + _ipcManager.Penumbra.ModDirectory!, + _ipcManager.Penumbra.ModDirectory!.EndsWith('\\') + ? PenumbraPrefix + '\\' : PenumbraPrefix, + StringComparison.OrdinalIgnoreCase) + .Replace( + _configService.Current.CacheFolder, + _configService.Current.CacheFolder.EndsWith('\\') + ? CachePrefix + '\\' : CachePrefix, + StringComparison.OrdinalIgnoreCase) .Replace("\\\\", "\\", StringComparison.Ordinal); - if (!cleanedPaths.ContainsValue(cleaned)) + if (seenCleaned.TryAdd(cleaned, 0)) { _logger.LogDebug("Adding to cleanedPaths: {cleaned}", cleaned); cleanedPaths[p] = cleaned; - } else - { - _logger.LogWarning("Duplicated found: {cleaned}", cleaned); } - } + else + { + _logger.LogWarning("Duplicate found: {cleaned}", cleaned); + } + }); - Dictionary result = new(StringComparer.OrdinalIgnoreCase); + var result = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - var dict = _fileCaches.SelectMany(f => f.Value).Distinct() - .ToDictionary(d => d.PrefixedFilePath, d => d, StringComparer.OrdinalIgnoreCase); - - foreach (var entry in cleanedPaths) + Parallel.ForEach(cleanedPaths, entry => { _logger.LogDebug("Checking if in cache: {path}", entry.Value); - if (dict.TryGetValue(entry.Value, out var entity)) + if (cacheDict.TryGetValue(entry.Value, out var entity)) { var validatedCache = GetValidatedFileCache(entity); - result.Add(entry.Key, validatedCache); + result[entry.Key] = validatedCache; } else { if (!entry.Value.Contains(CachePrefix, StringComparison.Ordinal)) - result.Add(entry.Key, CreateFileEntry(entry.Key)); + result[entry.Key] = CreateFileEntry(entry.Key); else - result.Add(entry.Key, CreateCacheEntry(entry.Key)); + result[entry.Key] = CreateCacheEntry(entry.Key); } - } + }); - return result; + return new Dictionary(result, StringComparer.OrdinalIgnoreCase); } finally { @@ -450,7 +470,7 @@ public sealed class FileCacheManager : IHostedService { attempts++; _logger.LogWarning(ex, "Could not open {file}, trying again", _csvPath); - Thread.Sleep(100); + Task.Delay(100, cancellationToken); } } diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index cfe5277..002bf1a 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -43,6 +43,7 @@ public class LightlessConfig : ILightlessConfiguration public bool ShowCharacterNameInsteadOfNotesForVisible { get; set; } = false; public bool ShowOfflineUsersSeparately { get; set; } = true; public bool ShowSyncshellOfflineUsersSeparately { get; set; } = true; + public bool ShowGroupedSyncshellsInAll { get; set; } = true; public bool GroupUpSyncshells { get; set; } = true; public bool ShowOnlineNotifications { get; set; } = false; public bool ShowOnlineNotificationsOnlyForIndividualPairs { get; set; } = true; diff --git a/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs index d87f3c3..ca12006 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/PlayerPerformanceConfig.cs @@ -14,4 +14,6 @@ public class PlayerPerformanceConfig : ILightlessConfiguration public int TrisAutoPauseThresholdThousands { get; set; } = 250; public List UIDsToIgnore { get; set; } = new(); public bool PauseInInstanceDuty { get; set; } = false; + public bool PauseWhilePerforming { get; set; } = true; + public bool PauseInCombat { get; set; } = true; } \ No newline at end of file diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 4760422..7ce002d 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -3,7 +3,7 @@ - 1.11.9 + 1.11.10 https://github.com/Light-Public-Syncshells/LightlessClient diff --git a/LightlessSync/PlayerData/Handlers/PairHandler.cs b/LightlessSync/PlayerData/Handlers/PairHandler.cs index edde269..24cd87f 100644 --- a/LightlessSync/PlayerData/Handlers/PairHandler.cs +++ b/LightlessSync/PlayerData/Handlers/PairHandler.cs @@ -88,11 +88,19 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase _redrawOnNextApplication = true; } }); - Mediator.Subscribe(this, (msg) => + Mediator.Subscribe(this, (msg) => { EnableSync(); }); - Mediator.Subscribe(this, _ => + Mediator.Subscribe(this, _ => + { + DisableSync(); + }); + Mediator.Subscribe(this, (msg) => + { + EnableSync(); + }); + Mediator.Subscribe(this, _ => { DisableSync(); }); @@ -137,11 +145,21 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false) { - if (_dalamudUtil.IsInCombatOrPerforming) + if (_dalamudUtil.IsInCombat) { Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning, - "Cannot apply character data: you are in combat or performing music, deferring application"))); - Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat or performing", applicationBase); + "Cannot apply character data: you are in combat, deferring application"))); + Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase); + _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); + SetUploading(isUploading: false); + return; + } + + if (_dalamudUtil.IsPerforming) + { + Mediator.Publish(new EventMessage(new Event(PlayerName, Pair.UserData, nameof(PairHandler), EventSeverity.Warning, + "Cannot apply character data: you are performing music, deferring application"))); + Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase); _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); SetUploading(isUploading: false); return; diff --git a/LightlessSync/Services/CharaData/CharaDataManager.cs b/LightlessSync/Services/CharaData/CharaDataManager.cs index 0c6ed50..38ec1c7 100644 --- a/LightlessSync/Services/CharaData/CharaDataManager.cs +++ b/LightlessSync/Services/CharaData/CharaDataManager.cs @@ -944,9 +944,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase Logger.LogTrace("[{appId}] Computing local missing files", applicationId); - Dictionary modPaths; - List missingFiles; - _fileHandler.ComputeMissingFiles(charaDataDownloadDto, out modPaths, out missingFiles); + _fileHandler.ComputeMissingFiles(charaDataDownloadDto, out Dictionary modPaths, out List missingFiles); Logger.LogTrace("[{appId}] Computing local missing files", applicationId); @@ -990,7 +988,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase { _uploadCts = _uploadCts.CancelRecreate(); var missingFiles = await _fileHandler.UploadFiles([.. missingFileList.Select(k => k.HashOrFileSwap)], UploadProgress, _uploadCts.Token).ConfigureAwait(false); - if (missingFiles.Any()) + if (missingFiles.Count != 0) { Logger.LogInformation("Failed to upload {files}", string.Join(", ", missingFiles)); return ($"Upload failed: {missingFiles.Count} missing or forbidden to upload local files.", false); diff --git a/LightlessSync/Services/CharaData/CharaDataNearbyManager.cs b/LightlessSync/Services/CharaData/CharaDataNearbyManager.cs index 8b86e8e..b3d4800 100644 --- a/LightlessSync/Services/CharaData/CharaDataNearbyManager.cs +++ b/LightlessSync/Services/CharaData/CharaDataNearbyManager.cs @@ -236,7 +236,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase } } - if (_charaDataConfigService.Current.NearbyDrawWisps && !_dalamudUtilService.IsInGpose && !_dalamudUtilService.IsInCombatOrPerforming) + if (_charaDataConfigService.Current.NearbyDrawWisps && !_dalamudUtilService.IsInGpose && !_dalamudUtilService.IsInCombat && !_dalamudUtilService.IsPerforming && !_dalamudUtilService.IsInInstance) await _dalamudUtilService.RunOnFrameworkThread(() => ManageWispsNearby(previousPoses)).ConfigureAwait(false); } @@ -253,7 +253,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase return; } - if (!_charaDataConfigService.Current.NearbyDrawWisps || _dalamudUtilService.IsInGpose || _dalamudUtilService.IsInCombatOrPerforming) + if (!_charaDataConfigService.Current.NearbyDrawWisps || _dalamudUtilService.IsInGpose || _dalamudUtilService.IsInCombat || _dalamudUtilService.IsPerforming || _dalamudUtilService.IsInInstance) ClearAllVfx(); var camera = CameraManager.Instance()->CurrentCamera; diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 4c2bdd8..ea21af7 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -162,7 +162,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public bool IsLoggedIn { get; private set; } public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread; public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51]; - public bool IsInCombatOrPerforming { get; private set; } = false; + public bool IsInCombat { get; private set; } = false; + public bool IsPerforming { get; private set; } = false; public bool IsInInstance { get; private set; } = false; public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles; public uint ClassJobId => _classJobId!.Value; @@ -670,19 +671,33 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber Mediator.Publish(new GposeEndMessage()); } - if ((_condition[ConditionFlag.Performing] || _condition[ConditionFlag.InCombat]) && !IsInCombatOrPerforming && (_condition[ConditionFlag.BoundByDuty] && !_playerPerformanceConfigService.Current.PauseInInstanceDuty)) - { - _logger.LogDebug("Combat/Performance start"); - IsInCombatOrPerforming = true; - Mediator.Publish(new CombatOrPerformanceStartMessage()); - Mediator.Publish(new HaltScanMessage(nameof(IsInCombatOrPerforming))); + if ((_condition[ConditionFlag.InCombat]) && !IsInCombat && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat) + { + _logger.LogDebug("Combat start"); + IsInCombat = true; + Mediator.Publish(new CombatStartMessage()); + Mediator.Publish(new HaltScanMessage(nameof(IsInCombat))); } - else if ((!_condition[ConditionFlag.Performing] && !_condition[ConditionFlag.InCombat]) && IsInCombatOrPerforming && (_condition[ConditionFlag.BoundByDuty] && !_playerPerformanceConfigService.Current.PauseInInstanceDuty)) + else if ((!_condition[ConditionFlag.InCombat]) && IsInCombat && !IsInInstance && _playerPerformanceConfigService.Current.PauseInCombat) { - _logger.LogDebug("Combat/Performance end"); - IsInCombatOrPerforming = false; - Mediator.Publish(new CombatOrPerformanceEndMessage()); - Mediator.Publish(new ResumeScanMessage(nameof(IsInCombatOrPerforming))); + _logger.LogDebug("Combat end"); + IsInCombat = false; + Mediator.Publish(new CombatEndMessage()); + Mediator.Publish(new ResumeScanMessage(nameof(IsInCombat))); + } + if (_condition[ConditionFlag.Performing] && !IsPerforming && _playerPerformanceConfigService.Current.PauseWhilePerforming) + { + _logger.LogDebug("Performance start"); + IsInCombat = true; + Mediator.Publish(new PerformanceStartMessage()); + Mediator.Publish(new HaltScanMessage(nameof(IsPerforming))); + } + else if (!_condition[ConditionFlag.Performing] && IsPerforming && _playerPerformanceConfigService.Current.PauseWhilePerforming) + { + _logger.LogDebug("Performance end"); + IsInCombat = false; + Mediator.Publish(new PerformanceEndMessage()); + Mediator.Publish(new ResumeScanMessage(nameof(IsPerforming))); } if ((_condition[ConditionFlag.BoundByDuty]) && !IsInInstance && _playerPerformanceConfigService.Current.PauseInInstanceDuty) { @@ -752,7 +767,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber _classJobId = localPlayer.ClassJob.RowId; } - if (!IsInCombatOrPerforming) + if (!IsInCombat || !IsPerforming || !IsInInstance) Mediator.Publish(new FrameworkUpdateMessage()); Mediator.Publish(new PriorityFrameworkUpdateMessage()); @@ -781,7 +796,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber IsLodEnabled = lodEnabled; } - if (IsInCombatOrPerforming) + if (IsInCombat || IsPerforming || IsInInstance) Mediator.Publish(new FrameworkUpdateMessage()); Mediator.Publish(new DelayedFrameworkUpdateMessage()); diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index ea800d2..9018fe2 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -79,8 +79,10 @@ public record OpenPermissionWindow(Pair Pair) : MessageBase; public record DownloadLimitChangedMessage() : SameThreadMessage; public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase; public record TargetPairMessage(Pair Pair) : MessageBase; -public record CombatOrPerformanceStartMessage : MessageBase; -public record CombatOrPerformanceEndMessage : MessageBase; +public record CombatStartMessage : MessageBase; +public record CombatEndMessage : MessageBase; +public record PerformanceStartMessage : MessageBase; +public record PerformanceEndMessage : MessageBase; public record InstanceOrDutyStartMessage : MessageBase; public record InstanceOrDutyEndMessage : MessageBase; public record EventMessage(Event Event) : MessageBase; diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index e3913cb..6aa159d 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -8,7 +8,6 @@ using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.Interop.Ipc; using LightlessSync.LightlessConfiguration; -using LightlessSync.LightlessConfiguration.Configurations; using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; @@ -35,6 +34,7 @@ public class CompactUi : WindowMediatorSubscriberBase private readonly CharacterAnalyzer _characterAnalyzer; private readonly ApiController _apiController; private readonly LightlessConfigService _configService; + private readonly LightlessMediator _lightlessMediator; private readonly ConcurrentDictionary> _currentDownloads = new(); private readonly DrawEntityFactory _drawEntityFactory; private readonly FileUploadManager _fileTransferManager; @@ -57,7 +57,6 @@ public class CompactUi : WindowMediatorSubscriberBase private string _lastAddedUserComment = string.Empty; private Vector2 _lastPosition = Vector2.One; private Vector2 _lastSize = Vector2.One; - private int _secretKeyIdx = -1; private bool _showModalForUserAddition; private float _transferPartHeight; private bool _wasOpen; @@ -68,7 +67,7 @@ public class CompactUi : WindowMediatorSubscriberBase TagHandler tagHandler, DrawEntityFactory drawEntityFactory, SelectTagForPairUi selectTagForPairUi, SelectPairForTagUi selectPairForTagUi, RenamePairTagUi renameTagUi, SelectTagForSyncshellUi selectTagForSyncshellUi, SelectSyncshellForTagUi selectSyncshellForTagUi, RenameSyncshellTagUi renameSyncshellTagUi, - PerformanceCollectorService performanceCollectorService, IpcManager ipcManager, CharacterAnalyzer characterAnalyzer, PlayerPerformanceConfigService playerPerformanceConfig) + PerformanceCollectorService performanceCollectorService, IpcManager ipcManager, CharacterAnalyzer characterAnalyzer, PlayerPerformanceConfigService playerPerformanceConfig, LightlessMediator lightlessMediator) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) { _uiSharedService = uiShared; @@ -124,7 +123,7 @@ public class CompactUi : WindowMediatorSubscriberBase } }; - _drawFolders = GetDrawFolders().ToList(); + _drawFolders = [.. GetDrawFolders()]; #if DEBUG string dev = "Dev Build"; @@ -152,6 +151,7 @@ public class CompactUi : WindowMediatorSubscriberBase }; _characterAnalyzer = characterAnalyzer; _playerPerformanceConfig = playerPerformanceConfig; + _lightlessMediator = lightlessMediator; } protected override void DrawInternal() @@ -430,7 +430,7 @@ public class CompactUi : WindowMediatorSubscriberBase ImGui.SetClipboardText(uidText); } - if (_cachedAnalysis != null) + if (_cachedAnalysis != null && _apiController.ServerState is ServerState.Connected) { var firstEntry = _cachedAnalysis.FirstOrDefault(); var valueDict = firstEntry.Value; @@ -460,6 +460,7 @@ public class CompactUi : WindowMediatorSubscriberBase { ImGui.SameLine(); _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow")); + string warningMessage = ""; if (isOverTriHold) { @@ -474,6 +475,10 @@ public class CompactUi : WindowMediatorSubscriberBase $"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}."; } UiSharedService.AttachToolTip(warningMessage); + if (ImGui.IsItemClicked()) + { + _lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); + } } } } @@ -494,7 +499,7 @@ public class CompactUi : WindowMediatorSubscriberBase UiSharedService.AttachToolTip("Click to copy"); if (ImGui.IsItemClicked()) { - ImGui.SetClipboardText(_apiController.UID); + _lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); } } } @@ -543,6 +548,8 @@ public class CompactUi : WindowMediatorSubscriberBase => u.Value.Exists(g => string.Equals(g.GID, group.GID, StringComparison.Ordinal)); bool FilterNotTaggedUsers(KeyValuePair> u) => u.Key.IsDirectlyPaired && !u.Key.IsOneSidedPair && !_tagHandler.HasAnyPairTag(u.Key.UserData.UID); + bool FilterNotTaggedSyncshells(GroupFullInfoDto group) + => (!_tagHandler.HasAnySyncshellTag(group.GID) && !_configService.Current.ShowGroupedSyncshellsInAll) || _configService.Current.ShowGroupedSyncshellsInAll; bool FilterOfflineUsers(KeyValuePair> u) => ((u.Key.IsDirectlyPaired && _configService.Current.ShowSyncshellOfflineUsersSeparately) || !_configService.Current.ShowSyncshellOfflineUsersSeparately) @@ -566,7 +573,10 @@ public class CompactUi : WindowMediatorSubscriberBase foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)) { GetGroups(allPairs, filteredPairs, group, out ImmutableList allGroupPairs, out Dictionary> filteredGroupPairs); - groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs)); + if (FilterNotTaggedSyncshells(group)) + { + groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs)); + } } if (_configService.Current.GroupUpSyncshells) diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 2832dd6..2b9de1b 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -633,25 +633,6 @@ public class SettingsUi : WindowMediatorSubscriberBase { _lastTab = "FileCache"; - _uiShared.UnderlinedBigText("Export MCDF", UIColors.Get("LightlessBlue")); - - ImGuiHelpers.ScaledDummy(10); - - UiSharedService.ColorTextWrapped("Exporting MCDF has moved.", UIColors.Get("LightlessYellow")); - ImGuiHelpers.ScaledDummy(5); - UiSharedService.TextWrapped("It is now found in the Main UI under \"Your User Menu\" ("); - ImGui.SameLine(); - _uiShared.IconText(FontAwesomeIcon.UserCog); - ImGui.SameLine(); - UiSharedService.TextWrapped(") -> \"Character Data Hub\"."); - if (_uiShared.IconTextButton(FontAwesomeIcon.Running, "Open Lightless Character Data Hub")) - { - Mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi))); - } - UiSharedService.TextWrapped("Note: this entry will be removed in the near future. Please use the Main UI to open the Character Data Hub."); - ImGuiHelpers.ScaledDummy(5); - ImGui.Separator(); - _uiShared.UnderlinedBigText("Storage", UIColors.Get("LightlessBlue")); ImGuiHelpers.ScaledDummy(5); @@ -832,7 +813,14 @@ public class SettingsUi : WindowMediatorSubscriberBase { foreach (var file in Directory.GetFiles(_configService.Current.CacheFolder)) { - File.Delete(file); + try + { + File.Delete(file); + } + catch (IOException ex) + { + _logger.LogWarning(ex, $"Could not delete file {file} because it is in use."); + } } }); } @@ -939,6 +927,7 @@ public class SettingsUi : WindowMediatorSubscriberBase var preferNotesInsteadOfName = _configService.Current.PreferNotesOverNamesForVisible; var useFocusTarget = _configService.Current.UseFocusTarget; var groupUpSyncshells = _configService.Current.GroupUpSyncshells; + var groupedSyncshells = _configService.Current.ShowGroupedSyncshellsInAll; var groupInVisible = _configService.Current.ShowSyncshellUsersInVisible; var syncshellOfflineSeparate = _configService.Current.ShowSyncshellOfflineUsersSeparately; @@ -1156,6 +1145,14 @@ public class SettingsUi : WindowMediatorSubscriberBase } _uiShared.DrawHelpText("This will group up all Syncshells in a special 'All Syncshells' folder in the main UI."); + if (ImGui.Checkbox("Show grouped syncshells in main screen/all syncshells", ref groupedSyncshells)) + { + _configService.Current.ShowGroupedSyncshellsInAll = groupedSyncshells; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + _uiShared.DrawHelpText("This will show grouped syncshells in main screen or group 'All Syncshells'."); + if (ImGui.Checkbox("Show player name for visible players", ref showNameInsteadOfNotes)) { _configService.Current.ShowCharacterNameInsteadOfNotesForVisible = showNameInsteadOfNotes; @@ -1380,16 +1377,32 @@ public class SettingsUi : WindowMediatorSubscriberBase bool autoPause = _playerPerformanceConfigService.Current.AutoPausePlayersExceedingThresholds; bool autoPauseEveryone = _playerPerformanceConfigService.Current.AutoPausePlayersWithPreferredPermissionsExceedingThresholds; bool autoPauseInDuty = _playerPerformanceConfigService.Current.PauseInInstanceDuty; + bool autoPauseInCombat = _playerPerformanceConfigService.Current.PauseInCombat; + bool autoPauseWhilePerforming = _playerPerformanceConfigService.Current.PauseWhilePerforming; if (_uiShared.MediumTreeNode("Auto Pause", UIColors.Get("LightlessPurple"))) { + if (ImGui.Checkbox("Auto pause sync while combat", ref autoPauseInCombat)) + { + _playerPerformanceConfigService.Current.PauseInCombat = autoPauseInCombat; + _playerPerformanceConfigService.Save(); + } + _uiShared.DrawHelpText("AUTO-ENABLED: Your risk of crashing during a fight increases when this is disabled. For example: VFX mods Loading mid fight can cause a crash." + Environment.NewLine + + UiSharedService.TooltipSeparator + "WARNING: DISABLE AT YOUR OWN RISK."); + if (ImGui.Checkbox("Auto pause sync while in Perfomance as Bard", ref autoPauseWhilePerforming)) + { + _playerPerformanceConfigService.Current.PauseWhilePerforming = autoPauseWhilePerforming; + _playerPerformanceConfigService.Save(); + } + _uiShared.DrawHelpText("AUTO-ENABLED: Your risk of crashing during a performance increases when this is disabled. For example: Some mods can crash you mid performance" + Environment.NewLine + + UiSharedService.TooltipSeparator + "WARNING: DISABLE AT YOUR OWN RISK."); if (ImGui.Checkbox("Auto pause sync while in instances and duties", ref autoPauseInDuty)) { _playerPerformanceConfigService.Current.PauseInInstanceDuty = autoPauseInDuty; _playerPerformanceConfigService.Save(); } _uiShared.DrawHelpText("When enabled, it will automatically pause all players while you are in an instance, such as a dungeon or raid." + Environment.NewLine - + UiSharedService.TooltipSeparator + "Warning: You many have to leave the dungeon to resync with people again"); + + UiSharedService.TooltipSeparator + "Warning: You may have to leave the dungeon to resync with people again"); if (ImGui.Checkbox("Automatically pause players exceeding thresholds", ref autoPause)) { diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index 4cf254b..3dd90de 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -195,9 +195,9 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase using var table = ImRaii.Table("userList#" + GroupFullInfo.Group.GID, 3, tableFlags); if (table) { - ImGui.TableSetupColumn("Alias/UID/Note", ImGuiTableColumnFlags.None, 5); + ImGui.TableSetupColumn("Alias/UID/Note", ImGuiTableColumnFlags.None, 4); ImGui.TableSetupColumn("Flags", ImGuiTableColumnFlags.None, 1); - ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 2); + ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.None, 3); ImGui.TableHeadersRow(); var groupedPairs = new Dictionary(pairs.Select(p => new KeyValuePair(p, @@ -254,13 +254,32 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase ImGui.TableNextColumn(); // actions if (_isOwner) { - if (_uiSharedService.IconButton(FontAwesomeIcon.UserShield)) + using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"))) { - GroupPairUserInfo userInfo = pair.Value ?? GroupPairUserInfo.None; + using (ImRaii.Disabled(!UiSharedService.ShiftPressed())) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.Crown)) + { + _ = _apiController.GroupChangeOwnership(new(GroupFullInfo.Group, pair.Key.UserData)); + IsOpen = false; + } + } - userInfo.SetModerator(!userInfo.IsModerator()); + } + UiSharedService.AttachToolTip("Hold SHIFT and click to transfer ownership of this Syncshell to " + + (pair.Key.UserData.AliasOrUID) + Environment.NewLine + "WARNING: This action is irreversible and will close screen."); + ImGui.SameLine(); - _ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(GroupFullInfo.Group, pair.Key.UserData, userInfo)); + using (ImRaii.PushColor(ImGuiCol.Text, pair.Value != null && pair.Value.Value.IsModerator() ? UIColors.Get("DimRed") : UIColors.Get("PairBlue"))) + { + if (_uiSharedService.IconButton(FontAwesomeIcon.UserShield)) + { + GroupPairUserInfo userInfo = pair.Value ?? GroupPairUserInfo.None; + + userInfo.SetModerator(!userInfo.IsModerator()); + + _ = _apiController.GroupSetUserInfo(new GroupPairUserInfoDto(GroupFullInfo.Group, pair.Key.UserData, userInfo)); + } } UiSharedService.AttachToolTip(pair.Value != null && pair.Value.Value.IsModerator() ? "Demod user" : "Mod user"); ImGui.SameLine(); diff --git a/LightlessSync/WebAPI/Files/FileDownloadManager.cs b/LightlessSync/WebAPI/Files/FileDownloadManager.cs index 6932384..bc83ee5 100644 --- a/LightlessSync/WebAPI/Files/FileDownloadManager.cs +++ b/LightlessSync/WebAPI/Files/FileDownloadManager.cs @@ -144,28 +144,55 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase _downloadStatus[downloadGroup].DownloadStatus = DownloadStatus.Downloading; + const int maxRetries = 3; + int retryCount = 0; + TimeSpan retryDelay = TimeSpan.FromSeconds(2); + HttpResponseMessage response = null!; var requestUrl = LightlessFiles.CacheGetFullPath(fileTransfer[0].DownloadUri, requestId); - Logger.LogDebug("Downloading {requestUrl} for request {id}", requestUrl, requestId); - try + while (true) { - response = await _orchestrator.SendRequestAsync(HttpMethod.Get, requestUrl, ct, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); - } - catch (HttpRequestException ex) - { - Logger.LogWarning(ex, "Error during download of {requestUrl}, HttpStatusCode: {code}", requestUrl, ex.StatusCode); - if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized) + try { - throw new InvalidDataException($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex); + Logger.LogDebug("Attempt {attempt} - Downloading {requestUrl} for request {id}", retryCount + 1, requestUrl, requestId); + + response = await _orchestrator.SendRequestAsync(HttpMethod.Get, requestUrl, ct, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + break; + } + catch (HttpRequestException ex) when (ex.InnerException is TimeoutException || ex.StatusCode == null) + { + retryCount++; + + Logger.LogWarning(ex, "Timeout during download of {requestUrl}. Attempt {attempt} of {maxRetries}", requestUrl, retryCount, maxRetries); + + if (retryCount >= maxRetries || ct.IsCancellationRequested) + { + Logger.LogError($"Max retries reached or cancelled. Failing download for {requestUrl}"); + throw; + } + + await Task.Delay(retryDelay, ct).ConfigureAwait(false); // Wait before retrying + } + catch (HttpRequestException ex) + { + Logger.LogWarning(ex, "Error during download of {requestUrl}, HttpStatusCode: {code}", requestUrl, ex.StatusCode); + + if (ex.StatusCode is HttpStatusCode.NotFound or HttpStatusCode.Unauthorized) + { + throw new InvalidDataException($"Http error {ex.StatusCode} (cancelled: {ct.IsCancellationRequested}): {requestUrl}", ex); + } + + throw; } } - ThrottledStream? stream = null; + FileStream? fileStream = null; + try { - var fileStream = File.Create(tempPath); + fileStream = File.Create(tempPath); await using (fileStream.ConfigureAwait(false)) { var bufferSize = response.Content.Headers.ContentLength > 1024 * 1024 ? 65536 : 8196; @@ -174,8 +201,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase var bytesRead = 0; var limit = _orchestrator.DownloadLimitPerSlot(); Logger.LogTrace("Starting Download of {id} with a speed limit of {limit} to {tempPath}", requestId, limit, tempPath); - stream = new ThrottledStream(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), limit); + + stream = new(await response.Content.ReadAsStreamAsync(ct).ConfigureAwait(false), limit); + _activeDownloadStreams.Add(stream); + while ((bytesRead = await stream.ReadAsync(buffer, ct).ConfigureAwait(false)) > 0) { ct.ThrowIfCancellationRequested(); @@ -194,18 +224,22 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase { throw; } - catch (Exception ex) + catch (Exception) { try { - if (!tempPath.IsNullOrEmpty()) + fileStream?.Close(); + + if (!string.IsNullOrEmpty(tempPath) && File.Exists(tempPath)) + { File.Delete(tempPath); + } } catch { - // ignore if file deletion fails + // Ignore errors during cleanup } - throw; + throw; } finally { diff --git a/LightlessSync/WebAPI/Files/FileUploadManager.cs b/LightlessSync/WebAPI/Files/FileUploadManager.cs index 3a78991..887cf9b 100644 --- a/LightlessSync/WebAPI/Files/FileUploadManager.cs +++ b/LightlessSync/WebAPI/Files/FileUploadManager.cs @@ -69,7 +69,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase Logger.LogDebug("Trying to upload files"); var filesPresentLocally = hashesToUpload.Where(h => _fileDbManager.GetFileCacheByHash(h) != null).ToHashSet(StringComparer.Ordinal); var locallyMissingFiles = hashesToUpload.Except(filesPresentLocally, StringComparer.Ordinal).ToList(); - if (locallyMissingFiles.Any()) + if (locallyMissingFiles.Count != 0) { return locallyMissingFiles; } @@ -92,7 +92,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase var data = await _fileDbManager.GetCompressedFileData(file.Hash, ct ?? CancellationToken.None).ConfigureAwait(false); Logger.LogDebug("[{hash}] Starting upload for {filePath}", data.Item1, _fileDbManager.GetFileCacheByHash(data.Item1)!.ResolvedFilepath); await uploadTask.ConfigureAwait(false); - uploadTask = UploadFile(data.Item2, file.Hash, false, ct ?? CancellationToken.None); + uploadTask = UploadFile(data.Item2, file.Hash, postProgress: false, ct ?? CancellationToken.None); (ct ?? CancellationToken.None).ThrowIfCancellationRequested(); }