From 4444a88746d7f763335c6465f14946c3d923a16a Mon Sep 17 00:00:00 2001 From: azyges Date: Tue, 16 Dec 2025 06:31:29 +0900 Subject: [PATCH] watafak --- .../PlayerData/Pairs/IPairHandlerAdapter.cs | 9 + LightlessSync/PlayerData/Pairs/Pair.cs | 24 + .../PlayerData/Pairs/PairDebugInfo.cs | 32 ++ .../PlayerData/Pairs/PairHandlerAdapter.cs | 132 +++-- .../PlayerData/Pairs/PairHandlerRegistry.cs | 5 + .../ActorTracking/ActorObjectService.cs | 24 +- LightlessSync/Services/CharacterAnalyzer.cs | 48 +- LightlessSync/Services/DalamudUtilService.cs | 123 ++++- LightlessSync/Services/Events/Event.cs | 8 +- .../TextureCompressionCapabilities.cs | 10 +- .../TextureDownscaleService.cs | 31 ++ .../TextureMetadataHelper.cs | 136 +++-- LightlessSync/Services/UiFactory.cs | 98 ++-- LightlessSync/UI/CompactUI.cs | 130 +---- LightlessSync/UI/CreateSyncshellUI.cs | 12 +- LightlessSync/UI/DataAnalysisUi.cs | 28 +- LightlessSync/UI/EditProfileUi.cs | 30 +- LightlessSync/UI/EventViewerUI.cs | 9 +- LightlessSync/UI/IntroUI.cs | 9 +- LightlessSync/UI/LightFinderUI.cs | 9 +- LightlessSync/UI/PermissionWindowUI.cs | 11 +- LightlessSync/UI/SettingsUi.cs | 483 ++++++++++++++++-- LightlessSync/UI/StandaloneProfileUi.cs | 49 +- LightlessSync/UI/SyncshellFinderUI.cs | 8 +- LightlessSync/UI/TopTabMenu.cs | 31 +- LightlessSync/UI/UIColors.cs | 1 + LightlessSync/UI/UpdateNotesUi.cs | 14 +- LightlessSync/UI/ZoneChatUi.cs | 19 +- LightlessSync/Utils/WindowUtils.cs | 139 +++++ OtterGui | 2 +- Penumbra.Api | 2 +- Penumbra.String | 2 +- 32 files changed, 1204 insertions(+), 464 deletions(-) create mode 100644 LightlessSync/PlayerData/Pairs/PairDebugInfo.cs create mode 100644 LightlessSync/Utils/WindowUtils.cs diff --git a/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs index c89d311..a7bd80c 100644 --- a/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/IPairHandlerAdapter.cs @@ -16,6 +16,15 @@ public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject new string? PlayerName { get; } string PlayerNameHash { get; } uint PlayerCharacterId { get; } + DateTime? LastDataReceivedAt { get; } + DateTime? LastApplyAttemptAt { get; } + DateTime? LastSuccessfulApplyAt { get; } + string? LastFailureReason { get; } + IReadOnlyList LastBlockingConditions { get; } + bool IsApplying { get; } + bool IsDownloading { get; } + int PendingDownloadCount { get; } + int ForbiddenDownloadCount { get; } void Initialize(); void ApplyData(CharacterData data); diff --git a/LightlessSync/PlayerData/Pairs/Pair.cs b/LightlessSync/PlayerData/Pairs/Pair.cs index 0eda06a..7d780dd 100644 --- a/LightlessSync/PlayerData/Pairs/Pair.cs +++ b/LightlessSync/PlayerData/Pairs/Pair.cs @@ -189,4 +189,28 @@ public class Pair handler.SetUploading(true); } + + public PairDebugInfo GetDebugInfo() + { + var handler = TryGetHandler(); + if (handler is null) + { + return PairDebugInfo.Empty; + } + + return new PairDebugInfo( + true, + handler.Initialized, + handler.IsVisible, + handler.ScheduledForDeletion, + handler.LastDataReceivedAt, + handler.LastApplyAttemptAt, + handler.LastSuccessfulApplyAt, + handler.LastFailureReason, + handler.LastBlockingConditions, + handler.IsApplying, + handler.IsDownloading, + handler.PendingDownloadCount, + handler.ForbiddenDownloadCount); + } } diff --git a/LightlessSync/PlayerData/Pairs/PairDebugInfo.cs b/LightlessSync/PlayerData/Pairs/PairDebugInfo.cs new file mode 100644 index 0000000..9074c82 --- /dev/null +++ b/LightlessSync/PlayerData/Pairs/PairDebugInfo.cs @@ -0,0 +1,32 @@ +namespace LightlessSync.PlayerData.Pairs; + +public sealed record PairDebugInfo( + bool HasHandler, + bool HandlerInitialized, + bool HandlerVisible, + bool HandlerScheduledForDeletion, + DateTime? LastDataReceivedAt, + DateTime? LastApplyAttemptAt, + DateTime? LastSuccessfulApplyAt, + string? LastFailureReason, + IReadOnlyList BlockingConditions, + bool IsApplying, + bool IsDownloading, + int PendingDownloadCount, + int ForbiddenDownloadCount) +{ + public static PairDebugInfo Empty { get; } = new( + false, + false, + false, + false, + null, + null, + null, + null, + Array.Empty(), + false, + false, + 0, + 0); +} diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index f170bac..556dd84 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -65,6 +65,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa private readonly object _pauseLock = new(); private Task _pauseTransitionTask = Task.CompletedTask; private bool _pauseRequested; + private DateTime? _lastDataReceivedAt; + private DateTime? _lastApplyAttemptAt; + private DateTime? _lastSuccessfulApplyAt; + private string? _lastFailureReason; + private IReadOnlyList _lastBlockingConditions = Array.Empty(); public string Ident { get; } public bool Initialized { get; private set; } @@ -101,6 +106,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa public long LastAppliedApproximateVRAMBytes { get; set; } = -1; public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1; public CharacterData? LastReceivedCharacterData { get; private set; } + public DateTime? LastDataReceivedAt => _lastDataReceivedAt; + public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt; + public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt; + public string? LastFailureReason => _lastFailureReason; + public IReadOnlyList LastBlockingConditions => _lastBlockingConditions; + public bool IsApplying => _applicationTask is { IsCompleted: false }; + public bool IsDownloading => _downloadManager.IsDownloading; + public int PendingDownloadCount => _downloadManager.CurrentDownloads.Count; + public int ForbiddenDownloadCount => _downloadManager.ForbiddenTransfers.Count; public PairHandlerAdapter( ILogger logger, @@ -423,6 +437,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa { EnsureInitialized(); LastReceivedCharacterData = data; + _lastDataReceivedAt = DateTime.UtcNow; ApplyLastReceivedData(); } @@ -713,10 +728,26 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa && _ipcManager.Glamourer.APIAvailable; } + private void RecordFailure(string reason, params string[] conditions) + { + _lastFailureReason = reason; + _lastBlockingConditions = conditions.Length == 0 ? Array.Empty() : conditions.ToArray(); + } + + private void ClearFailureState() + { + _lastFailureReason = null; + _lastBlockingConditions = Array.Empty(); + } + public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false) { + _lastApplyAttemptAt = DateTime.UtcNow; + ClearFailureState(); + if (characterData is null) { + RecordFailure("Received null character data", "InvalidData"); Logger.LogWarning("[BASE-{appBase}] Received null character data, skipping application for {handler}", applicationBase, GetLogIdentifier()); SetUploading(false); return; @@ -725,9 +756,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa var user = GetPrimaryUserData(); if (_dalamudUtil.IsInCombat) { + const string reason = "Cannot apply character data: you are in combat, deferring application"; Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, - "Cannot apply character data: you are in combat, deferring application"))); + reason))); Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase); + RecordFailure(reason, "Combat"); _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); SetUploading(false); return; @@ -735,9 +768,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (_dalamudUtil.IsPerforming) { + const string reason = "Cannot apply character data: you are performing music, deferring application"; Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, - "Cannot apply character data: you are performing music, deferring application"))); + reason))); Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase); + RecordFailure(reason, "Performance"); _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); SetUploading(false); return; @@ -745,9 +780,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (_dalamudUtil.IsInInstance) { + const string reason = "Cannot apply character data: you are in an instance, deferring application"; Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, - "Cannot apply character data: you are in an instance, deferring application"))); + reason))); Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase); + RecordFailure(reason, "Instance"); _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); SetUploading(false); return; @@ -755,9 +792,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (_dalamudUtil.IsInCutscene) { + const string reason = "Cannot apply character data: you are in a cutscene, deferring application"; Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, - "Cannot apply character data: you are in a cutscene, deferring application"))); + reason))); Logger.LogDebug("[BASE-{appBase}] Received data but player is in a cutscene", applicationBase); + RecordFailure(reason, "Cutscene"); _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); SetUploading(false); return; @@ -765,9 +804,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (_dalamudUtil.IsInGpose) { + const string reason = "Cannot apply character data: you are in GPose, deferring application"; Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, - "Cannot apply character data: you are in GPose, deferring application"))); + reason))); Logger.LogDebug("[BASE-{appBase}] Received data but player is in GPose", applicationBase); + RecordFailure(reason, "GPose"); _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); SetUploading(false); return; @@ -775,9 +816,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable) { + const string reason = "Cannot apply character data: Penumbra or Glamourer is not available, deferring application"; Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, - "Cannot apply character data: Penumbra or Glamourer is not available, deferring application"))); + reason))); Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier()); + RecordFailure(reason, "PluginUnavailable"); _dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization); SetUploading(false); return; @@ -1260,6 +1303,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles)) { + RecordFailure("Auto pause triggered by VRAM usage thresholds", "VRAMThreshold"); _downloadManager.ClearDownload(); return; } @@ -1272,9 +1316,24 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (downloadToken.IsCancellationRequested) { Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase); + RecordFailure("Download cancelled", "Cancellation"); return; } + if (!skipDownscaleForPair) + { + var downloadedTextureHashes = toDownloadReplacements + .Where(static replacement => replacement.GamePaths.Any(static path => path.EndsWith(".tex", StringComparison.OrdinalIgnoreCase))) + .Select(static replacement => replacement.Hash) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (downloadedTextureHashes.Count > 0) + { + await _textureDownscaleService.WaitForPendingJobsAsync(downloadedTextureHashes, downloadToken).ConfigureAwait(false); + } + } + toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) @@ -1287,6 +1346,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false)) { + RecordFailure("Auto pause triggered by performance thresholds", "PerformanceThreshold"); return; } } @@ -1307,6 +1367,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _cachedData = charaData; _pairStateCache.Store(Ident, charaData); _forceFullReapply = true; + RecordFailure("Handler not available for application", "HandlerUnavailable"); return; } @@ -1322,6 +1383,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false)) { _forceFullReapply = true; + RecordFailure("Application cancelled", "Cancellation"); return; } @@ -1359,6 +1421,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _cachedData = charaData; _pairStateCache.Store(Ident, charaData); _forceFullReapply = true; + RecordFailure("Penumbra collection unavailable", "PenumbraUnavailable"); return; } } @@ -1378,6 +1441,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _cachedData = charaData; _pairStateCache.Store(Ident, charaData); _forceFullReapply = true; + RecordFailure("Game object not available for application", "GameObjectUnavailable"); return; } @@ -1414,41 +1478,45 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _needsCollectionRebuild = false; if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0) { - _playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List()); - } - if (LastAppliedDataTris < 0) - { - await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false); - } - - StorePerformanceMetrics(charaData); - Logger.LogDebug("[{applicationId}] Application finished", _applicationId); + _playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List()); } - catch (OperationCanceledException) + if (LastAppliedDataTris < 0) { - Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier()); + await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false); + } + + StorePerformanceMetrics(charaData); + _lastSuccessfulApplyAt = DateTime.UtcNow; + ClearFailureState(); + Logger.LogDebug("[{applicationId}] Application finished", _applicationId); + } + catch (OperationCanceledException) + { + Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier()); + _cachedData = charaData; + _pairStateCache.Store(Ident, charaData); + _forceFullReapply = true; + RecordFailure("Application cancelled", "Cancellation"); + } + catch (Exception ex) + { + if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException)) + { + IsVisible = false; + _forceApplyMods = true; _cachedData = charaData; _pairStateCache.Store(Ident, charaData); _forceFullReapply = true; + Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId); } - catch (Exception ex) + else { - if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException)) - { - IsVisible = false; - _forceApplyMods = true; - _cachedData = charaData; - _pairStateCache.Store(Ident, charaData); - _forceFullReapply = true; - Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId); - } - else - { - Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId); - _forceFullReapply = true; - } + Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId); + _forceFullReapply = true; } + RecordFailure($"Application failed: {ex.Message}", "Exception"); } +} private void FrameworkUpdate() { diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs b/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs index bf700e8..f490804 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerRegistry.cs @@ -69,6 +69,10 @@ public sealed class PairHandlerRegistry : IDisposable handler = entry.Handler; handler.ScheduledForDeletion = false; entry.AddPair(registration.PairIdent); + if (!handler.Initialized) + { + handler.Initialize(); + } } ApplyPauseStateForHandler(handler); @@ -169,6 +173,7 @@ public sealed class PairHandlerRegistry : IDisposable return PairOperationResult.Ok(); } + handler.ApplyData(dto.CharaData); return PairOperationResult.Ok(); } diff --git a/LightlessSync/Services/ActorTracking/ActorObjectService.cs b/LightlessSync/Services/ActorTracking/ActorObjectService.cs index c2650ad..1813947 100644 --- a/LightlessSync/Services/ActorTracking/ActorObjectService.cs +++ b/LightlessSync/Services/ActorTracking/ActorObjectService.cs @@ -230,7 +230,12 @@ public sealed class ActorObjectService : IHostedService, IDisposable if (descriptor.ObjectKind == DalamudObjectKind.Player && !string.IsNullOrEmpty(descriptor.HashedContentId)) { - var liveHash = DalamudUtilService.GetHashedCIDFromPlayerPointer(descriptor.Address); + if (!TryGetLivePlayerHash(descriptor, out var liveHash)) + { + UntrackGameObject(descriptor.Address); + return false; + } + if (!string.Equals(liveHash, descriptor.HashedContentId, StringComparison.Ordinal)) { UntrackGameObject(descriptor.Address); @@ -241,6 +246,16 @@ public sealed class ActorObjectService : IHostedService, IDisposable return true; } + private bool TryGetLivePlayerHash(ActorDescriptor descriptor, out string liveHash) + { + liveHash = string.Empty; + + if (_objectTable.CreateObjectReference(descriptor.Address) is not IPlayerCharacter playerCharacter) + return false; + + return DalamudUtilService.TryGetHashedCID(playerCharacter, out liveHash); + } + public void RefreshTrackedActors(bool force = false) { var now = DateTime.UtcNow; @@ -425,8 +440,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable bool isLocal = _objectTable.LocalPlayer?.Address == address; string hashedCid = string.Empty; + IPlayerCharacter? resolvedPlayer = null; if (_objectTable.CreateObjectReference(address) is IPlayerCharacter playerCharacter) { + resolvedPlayer = playerCharacter; name = playerCharacter.Name.TextValue ?? string.Empty; objectIndex = playerCharacter.ObjectIndex; isInGpose = objectIndex >= 200; @@ -439,7 +456,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable if (objectKind == DalamudObjectKind.Player) { - hashedCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address); + if (resolvedPlayer == null || !DalamudUtilService.TryGetHashedCID(resolvedPlayer, out hashedCid)) + { + hashedCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address); + } } var (ownedKind, ownerEntityId) = DetermineOwnedKind(gameObject, objectKind, isLocal); diff --git a/LightlessSync/Services/CharacterAnalyzer.cs b/LightlessSync/Services/CharacterAnalyzer.cs index 2a0aa04..3eebced 100644 --- a/LightlessSync/Services/CharacterAnalyzer.cs +++ b/LightlessSync/Services/CharacterAnalyzer.cs @@ -250,32 +250,40 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable } OriginalSize = normalSize; CompressedSize = compressedsize.Item2.LongLength; + RefreshFormat(); } public long OriginalSize { get; private set; } = OriginalSize; public long CompressedSize { get; private set; } = CompressedSize; public long Triangles { get; private set; } = Triangles; - public Lazy Format = new(() => + public Lazy Format => _format ??= CreateFormatValue(); + + private Lazy? _format; + + public void RefreshFormat() { - switch (FileType) + _format = CreateFormatValue(); + } + + private Lazy CreateFormatValue() + => new(() => { - case "tex": - { - try - { - using var stream = new FileStream(FilePaths[0], FileMode.Open, FileAccess.Read, FileShare.Read); - using var reader = new BinaryReader(stream); - reader.BaseStream.Position = 4; - var format = (TexFile.TextureFormat)reader.ReadInt32(); - return format.ToString(); - } - catch - { - return "Unknown"; - } - } - default: + if (!string.Equals(FileType, "tex", StringComparison.Ordinal)) + { return string.Empty; - } - }); + } + + try + { + using var stream = new FileStream(FilePaths[0], FileMode.Open, FileAccess.Read, FileShare.Read); + using var reader = new BinaryReader(stream); + reader.BaseStream.Position = 4; + var format = (TexFile.TextureFormat)reader.ReadInt32(); + return format.ToString(); + } + catch + { + return "Unknown"; + } + }); } } diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index fdf2ec3..253847c 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -324,7 +324,28 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber EnsureIsOnFramework(); playerPointer ??= GetPlayerPtr(); if (playerPointer == IntPtr.Zero) return IntPtr.Zero; - return _objectTable.GetObjectAddress(((GameObject*)playerPointer)->ObjectIndex + 1); + + var playerAddress = playerPointer.Value; + var ownerEntityId = ((Character*)playerAddress)->EntityId; + if (ownerEntityId == 0) return IntPtr.Zero; + + if (playerAddress == _actorObjectService.LocalPlayerAddress) + { + var localOwned = _actorObjectService.LocalMinionOrMountAddress; + if (localOwned != nint.Zero) + { + return localOwned; + } + } + + var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind => + kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion); + if (ownedObject != nint.Zero) + { + return ownedObject; + } + + return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1); } public async Task GetMinionOrMountAsync(IntPtr? playerPointer = null) @@ -347,6 +368,62 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber return await RunOnFrameworkThread(() => GetPetPtr(playerPointer)).ConfigureAwait(false); } + private unsafe nint FindOwnedObject(uint ownerEntityId, nint ownerAddress, Func matchesKind) + { + if (ownerEntityId == 0) + { + return nint.Zero; + } + + foreach (var obj in _objectTable) + { + if (obj is null || obj.Address == nint.Zero || obj.Address == ownerAddress) + { + continue; + } + + if (!matchesKind(obj.ObjectKind)) + { + continue; + } + + var candidate = (GameObject*)obj.Address; + if (ResolveOwnerId(candidate) == ownerEntityId) + { + return obj.Address; + } + } + + return nint.Zero; + } + + private static unsafe uint ResolveOwnerId(GameObject* gameObject) + { + if (gameObject == null) + { + return 0; + } + + if (gameObject->OwnerId != 0) + { + return gameObject->OwnerId; + } + + var character = (Character*)gameObject; + if (character == null) + { + return 0; + } + + if (character->CompanionOwnerId != 0) + { + return character->CompanionOwnerId; + } + + var parent = character->GetParentCharacter(); + return parent != null ? parent->EntityId : 0; + } + public async Task GetPlayerCharacterAsync() { return await RunOnFrameworkThread(GetPlayerCharacter).ConfigureAwait(false); @@ -393,6 +470,24 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber return await RunOnFrameworkThread(() => _cid.Value.ToString().GetHash256()).ConfigureAwait(false); } + public static unsafe bool TryGetHashedCID(IPlayerCharacter? playerCharacter, out string hashedCid) + { + hashedCid = string.Empty; + if (playerCharacter == null) + return false; + + var address = playerCharacter.Address; + if (address == nint.Zero) + return false; + + var cid = ((BattleChara*)address)->Character.ContentId; + if (cid == 0) + return false; + + hashedCid = cid.ToString().GetHash256(); + return true; + } + public unsafe static string GetHashedCIDFromPlayerPointer(nint ptr) { return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256(); @@ -516,17 +611,13 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber var fileName = Path.GetFileNameWithoutExtension(callerFilePath); await _performanceCollector.LogPerformance(this, $"RunOnFramework:Act/{fileName}>{callerMember}:{callerLineNumber}", async () => { - if (!_framework.IsInFrameworkUpdateThread) + if (_framework.IsInFrameworkUpdateThread) { - await _framework.RunOnFrameworkThread(act).ContinueWith((_) => Task.CompletedTask).ConfigureAwait(false); - while (_framework.IsInFrameworkUpdateThread) // yield the thread again, should technically never be triggered - { - _logger.LogTrace("Still on framework"); - await Task.Delay(1).ConfigureAwait(false); - } - } - else act(); + return; + } + + await _framework.RunOnFrameworkThread(act).ConfigureAwait(false); }).ConfigureAwait(false); } @@ -535,18 +626,12 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber var fileName = Path.GetFileNameWithoutExtension(callerFilePath); return await _performanceCollector.LogPerformance(this, $"RunOnFramework:Func<{typeof(T)}>/{fileName}>{callerMember}:{callerLineNumber}", async () => { - if (!_framework.IsInFrameworkUpdateThread) + if (_framework.IsInFrameworkUpdateThread) { - var result = await _framework.RunOnFrameworkThread(func).ContinueWith((task) => task.Result).ConfigureAwait(false); - while (_framework.IsInFrameworkUpdateThread) // yield the thread again, should technically never be triggered - { - _logger.LogTrace("Still on framework"); - await Task.Delay(1).ConfigureAwait(false); - } - return result; + return func.Invoke(); } - return func.Invoke(); + return await _framework.RunOnFrameworkThread(func).ConfigureAwait(false); }).ConfigureAwait(false); } diff --git a/LightlessSync/Services/Events/Event.cs b/LightlessSync/Services/Events/Event.cs index ca540b9..9725d49 100644 --- a/LightlessSync/Services/Events/Event.cs +++ b/LightlessSync/Services/Events/Event.cs @@ -6,6 +6,8 @@ public record Event { public DateTime EventTime { get; } public string UID { get; } + public string AliasOrUid { get; } + public string UserId { get; } public string Character { get; } public string EventSource { get; } public EventSeverity EventSeverity { get; } @@ -14,7 +16,9 @@ public record Event public Event(string? Character, UserData UserData, string EventSource, EventSeverity EventSeverity, string Message) { EventTime = DateTime.Now; - this.UID = UserData.AliasOrUID; + this.UserId = UserData.UID; + this.AliasOrUid = UserData.AliasOrUID; + this.UID = UserData.UID; this.Character = Character ?? string.Empty; this.EventSource = EventSource; this.EventSeverity = EventSeverity; @@ -37,7 +41,7 @@ public record Event else { if (string.IsNullOrEmpty(Character)) - return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t<{UID}> {Message}"; + return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t<{AliasOrUid}> {Message}"; else return $"{EventTime:HH:mm:ss.fff}\t[{EventSource}]{{{(int)EventSeverity}}}\t<{UID}\\{Character}> {Message}"; } diff --git a/LightlessSync/Services/TextureCompression/TextureCompressionCapabilities.cs b/LightlessSync/Services/TextureCompression/TextureCompressionCapabilities.cs index 8725d07..bba27cd 100644 --- a/LightlessSync/Services/TextureCompression/TextureCompressionCapabilities.cs +++ b/LightlessSync/Services/TextureCompression/TextureCompressionCapabilities.cs @@ -8,15 +8,21 @@ internal static class TextureCompressionCapabilities private static readonly ImmutableDictionary TexTargets = new Dictionary { - [TextureCompressionTarget.BC7] = TextureType.Bc7Tex, + [TextureCompressionTarget.BC1] = TextureType.Bc1Tex, [TextureCompressionTarget.BC3] = TextureType.Bc3Tex, + [TextureCompressionTarget.BC4] = TextureType.Bc4Tex, + [TextureCompressionTarget.BC5] = TextureType.Bc5Tex, + [TextureCompressionTarget.BC7] = TextureType.Bc7Tex, }.ToImmutableDictionary(); private static readonly ImmutableDictionary DdsTargets = new Dictionary { - [TextureCompressionTarget.BC7] = TextureType.Bc7Dds, + [TextureCompressionTarget.BC1] = TextureType.Bc1Dds, [TextureCompressionTarget.BC3] = TextureType.Bc3Dds, + [TextureCompressionTarget.BC4] = TextureType.Bc4Dds, + [TextureCompressionTarget.BC5] = TextureType.Bc5Dds, + [TextureCompressionTarget.BC7] = TextureType.Bc7Dds, }.ToImmutableDictionary(); private static readonly TextureCompressionTarget[] SelectableTargetsCache = TexTargets diff --git a/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs b/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs index a7b42f5..7a09ae7 100644 --- a/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs +++ b/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs @@ -104,6 +104,37 @@ public sealed class TextureDownscaleService return originalPath; } + public Task WaitForPendingJobsAsync(IEnumerable? hashes, CancellationToken token) + { + if (hashes is null) + { + return Task.CompletedTask; + } + + var pending = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var hash in hashes) + { + if (string.IsNullOrEmpty(hash) || !seen.Add(hash)) + { + continue; + } + + if (_activeJobs.TryGetValue(hash, out var job)) + { + pending.Add(job); + } + } + + if (pending.Count == 0) + { + return Task.CompletedTask; + } + + return Task.WhenAll(pending).WaitAsync(token); + } + private async Task DownscaleInternalAsync(string hash, string sourcePath, TextureMapKind mapKind) { TexHeaderInfo? headerInfo = null; diff --git a/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs b/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs index 010f9be..f360ba3 100644 --- a/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs +++ b/LightlessSync/Services/TextureCompression/TextureMetadataHelper.cs @@ -88,26 +88,39 @@ public sealed class TextureMetadataHelper private static readonly (TextureMapKind Kind, string Token)[] MapTokens = { - (TextureMapKind.Normal, "_n"), + (TextureMapKind.Normal, "_n."), + (TextureMapKind.Normal, "_n_"), (TextureMapKind.Normal, "_normal"), + (TextureMapKind.Normal, "normal_"), (TextureMapKind.Normal, "_norm"), + (TextureMapKind.Normal, "norm_"), - (TextureMapKind.Mask, "_m"), + (TextureMapKind.Mask, "_m."), + (TextureMapKind.Mask, "_m_"), (TextureMapKind.Mask, "_mask"), + (TextureMapKind.Mask, "mask_"), (TextureMapKind.Mask, "_msk"), - (TextureMapKind.Specular, "_s"), + (TextureMapKind.Specular, "_s."), + (TextureMapKind.Specular, "_s_"), (TextureMapKind.Specular, "_spec"), + (TextureMapKind.Specular, "_specular"), + (TextureMapKind.Specular, "specular_"), - (TextureMapKind.Index, "_id"), + (TextureMapKind.Index, "_id."), + (TextureMapKind.Index, "_id_"), (TextureMapKind.Index, "_idx"), (TextureMapKind.Index, "_index"), + (TextureMapKind.Index, "index_"), (TextureMapKind.Index, "_multi"), - (TextureMapKind.Diffuse, "_d"), + (TextureMapKind.Diffuse, "_d."), + (TextureMapKind.Diffuse, "_d_"), (TextureMapKind.Diffuse, "_diff"), - (TextureMapKind.Diffuse, "_b"), - (TextureMapKind.Diffuse, "_base") + (TextureMapKind.Diffuse, "_b."), + (TextureMapKind.Diffuse, "_b_"), + (TextureMapKind.Diffuse, "_base"), + (TextureMapKind.Diffuse, "base_") }; private const string TextureSegment = "/texture/"; @@ -376,73 +389,83 @@ public sealed class TextureMetadataHelper private static TextureMapKind GuessMapFromFileName(string path) { var normalized = Normalize(path); - var fileName = Path.GetFileNameWithoutExtension(normalized); - if (string.IsNullOrEmpty(fileName)) + var fileNameWithExtension = Path.GetFileName(normalized); + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(normalized); + if (string.IsNullOrEmpty(fileNameWithExtension) && string.IsNullOrEmpty(fileNameWithoutExtension)) return TextureMapKind.Unknown; foreach (var (kind, token) in MapTokens) { - if (fileName.Contains(token, StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(fileNameWithExtension) && + fileNameWithExtension.Contains(token, StringComparison.OrdinalIgnoreCase)) + return kind; + + if (!string.IsNullOrEmpty(fileNameWithoutExtension) && + fileNameWithoutExtension.Contains(token, StringComparison.OrdinalIgnoreCase)) return kind; } return TextureMapKind.Unknown; } + private static readonly (string Token, TextureCompressionTarget Target)[] FormatTargetTokens = + { + ("BC1", TextureCompressionTarget.BC1), + ("DXT1", TextureCompressionTarget.BC1), + ("BC3", TextureCompressionTarget.BC3), + ("DXT3", TextureCompressionTarget.BC3), + ("DXT5", TextureCompressionTarget.BC3), + ("BC4", TextureCompressionTarget.BC4), + ("ATI1", TextureCompressionTarget.BC4), + ("BC5", TextureCompressionTarget.BC5), + ("ATI2", TextureCompressionTarget.BC5), + ("3DC", TextureCompressionTarget.BC5), + ("BC7", TextureCompressionTarget.BC7), + ("BPTC", TextureCompressionTarget.BC7) + }; // idk man + public static bool TryMapFormatToTarget(string? format, out TextureCompressionTarget target) { var normalized = (format ?? string.Empty).ToUpperInvariant(); - if (normalized.Contains("BC1", StringComparison.Ordinal)) + foreach (var (token, mappedTarget) in FormatTargetTokens) { - target = TextureCompressionTarget.BC1; - return true; - } - - if (normalized.Contains("BC3", StringComparison.Ordinal)) - { - target = TextureCompressionTarget.BC3; - return true; - } - - if (normalized.Contains("BC4", StringComparison.Ordinal)) - { - target = TextureCompressionTarget.BC4; - return true; - } - - if (normalized.Contains("BC5", StringComparison.Ordinal)) - { - target = TextureCompressionTarget.BC5; - return true; - } - - if (normalized.Contains("BC7", StringComparison.Ordinal)) - { - target = TextureCompressionTarget.BC7; - return true; + if (normalized.Contains(token, StringComparison.Ordinal)) + { + target = mappedTarget; + return true; + } } target = TextureCompressionTarget.BC7; return false; } - public static (TextureCompressionTarget Target, string Reason)? GetSuggestedTarget(string? format, TextureMapKind mapKind) + public static (TextureCompressionTarget Target, string Reason)? GetSuggestedTarget( + string? format, + TextureMapKind mapKind, + string? texturePath = null) { TextureCompressionTarget? current = null; if (TryMapFormatToTarget(format, out var mapped)) current = mapped; + var prefersBc4 = IsFacePaintOrMarkTexture(texturePath); + var suggestion = mapKind switch { TextureMapKind.Normal => TextureCompressionTarget.BC7, - TextureMapKind.Mask => TextureCompressionTarget.BC4, - TextureMapKind.Index => TextureCompressionTarget.BC3, - TextureMapKind.Specular => TextureCompressionTarget.BC4, + TextureMapKind.Mask => TextureCompressionTarget.BC7, + TextureMapKind.Index => TextureCompressionTarget.BC5, + TextureMapKind.Specular => TextureCompressionTarget.BC3, TextureMapKind.Diffuse => TextureCompressionTarget.BC7, _ => TextureCompressionTarget.BC7 }; - if (mapKind == TextureMapKind.Diffuse && !HasAlphaHint(format)) + if (prefersBc4) + { + suggestion = TextureCompressionTarget.BC4; + } + else if (mapKind == TextureMapKind.Diffuse && current is null && !HasAlphaHint(format)) suggestion = TextureCompressionTarget.BC1; if (current == suggestion) @@ -498,14 +521,41 @@ public sealed class TextureMetadataHelper || normalized.Contains("skin", StringComparison.OrdinalIgnoreCase) || normalized.Contains("bibo", StringComparison.OrdinalIgnoreCase)) return "Skin"; - if (normalized.Contains("decal_face", StringComparison.OrdinalIgnoreCase)) + if (IsFacePaintPath(normalized)) return "Face Paint"; + if (IsLegacyMarkPath(normalized)) + return "Legacy Mark"; if (normalized.Contains("decal_equip", StringComparison.OrdinalIgnoreCase)) return "Equipment Decal"; return "Customization"; } + private static bool IsFacePaintOrMarkTexture(string? texturePath) + { + var normalized = Normalize(texturePath); + return IsFacePaintPath(normalized) || IsLegacyMarkPath(normalized); + } + + private static bool IsFacePaintPath(string? normalizedPath) + { + if (string.IsNullOrEmpty(normalizedPath)) + return false; + + return normalizedPath.Contains("decal_face", StringComparison.Ordinal) + || normalizedPath.Contains("facepaint", StringComparison.Ordinal) + || normalizedPath.Contains("_decal_", StringComparison.Ordinal); + } + + private static bool IsLegacyMarkPath(string? normalizedPath) + { + if (string.IsNullOrEmpty(normalizedPath)) + return false; + + return normalizedPath.Contains("transparent", StringComparison.Ordinal) + || normalizedPath.Contains("transparent.tex", StringComparison.Ordinal); + } + private static bool HasAlphaHint(string? format) { if (string.IsNullOrEmpty(format)) diff --git a/LightlessSync/Services/UiFactory.cs b/LightlessSync/Services/UiFactory.cs index 7237936..9b90830 100644 --- a/LightlessSync/Services/UiFactory.cs +++ b/LightlessSync/Services/UiFactory.cs @@ -1,4 +1,3 @@ -using Dalamud.Interface.ImGuiFileDialog; using LightlessSync.API.Data; using LightlessSync.API.Dto.Group; using LightlessSync.PlayerData.Pairs; @@ -23,6 +22,7 @@ public class UiFactory private readonly LightlessProfileManager _lightlessProfileManager; private readonly PerformanceCollectorService _performanceCollectorService; private readonly ProfileTagService _profileTagService; + private readonly DalamudUtilService _dalamudUtilService; public UiFactory( ILoggerFactory loggerFactory, @@ -33,7 +33,8 @@ public class UiFactory ServerConfigurationManager serverConfigManager, LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService, - ProfileTagService profileTagService) + ProfileTagService profileTagService, + DalamudUtilService dalamudUtilService) { _loggerFactory = loggerFactory; _lightlessMediator = lightlessMediator; @@ -44,6 +45,7 @@ public class UiFactory _lightlessProfileManager = lightlessProfileManager; _performanceCollectorService = performanceCollectorService; _profileTagService = profileTagService; + _dalamudUtilService = dalamudUtilService; } public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto) @@ -60,76 +62,16 @@ public class UiFactory } public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair) - { - return new StandaloneProfileUi( - _loggerFactory.CreateLogger(), - _lightlessMediator, - _uiSharedService, - _serverConfigManager, - _profileTagService, - _lightlessProfileManager, - _pairUiService, - pair, - pair.UserData, - null, - false, - null, - _performanceCollectorService); - } + => CreateStandaloneProfileUiInternal(pair, pair.UserData, null, false, null); public StandaloneProfileUi CreateStandaloneProfileUi(UserData userData) - { - return new StandaloneProfileUi( - _loggerFactory.CreateLogger(), - _lightlessMediator, - _uiSharedService, - _serverConfigManager, - _profileTagService, - _lightlessProfileManager, - _pairUiService, - null, - userData, - null, - false, - null, - _performanceCollectorService); - } + => CreateStandaloneProfileUiInternal(null, userData, null, false, null); public StandaloneProfileUi CreateLightfinderProfileUi(UserData userData, string hashedCid) - { - return new StandaloneProfileUi( - _loggerFactory.CreateLogger(), - _lightlessMediator, - _uiSharedService, - _serverConfigManager, - _profileTagService, - _lightlessProfileManager, - _pairUiService, - null, - userData, - null, - true, - hashedCid, - _performanceCollectorService); - } + => CreateStandaloneProfileUiInternal(null, userData, null, true, hashedCid); public StandaloneProfileUi CreateStandaloneGroupProfileUi(GroupData groupInfo) - { - return new StandaloneProfileUi( - _loggerFactory.CreateLogger(), - _lightlessMediator, - _uiSharedService, - _serverConfigManager, - _profileTagService, - _lightlessProfileManager, - _pairUiService, - null, - null, - groupInfo, - false, - null, - _performanceCollectorService); - } + => CreateStandaloneProfileUiInternal(null, null, groupInfo, false, null); public PermissionWindowUI CreatePermissionPopupUi(Pair pair) { @@ -141,4 +83,28 @@ public class UiFactory _apiController, _performanceCollectorService); } + + private StandaloneProfileUi CreateStandaloneProfileUiInternal( + Pair? pair, + UserData? userData, + GroupData? groupData, + bool isLightfinderContext, + string? lightfinderCid) + { + return new StandaloneProfileUi( + _loggerFactory.CreateLogger(), + _lightlessMediator, + _uiSharedService, + _serverConfigManager, + _profileTagService, + dalamudUtilService: _dalamudUtilService, + lightlessProfileManager: _lightlessProfileManager, + pairUiService: _pairUiService, + pair: pair, + userData: userData, + groupData: groupData, + isLightfinderContext: isLightfinderContext, + lightfinderCid: lightfinderCid, + performanceCollector: _performanceCollectorService); + } } diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 65c6dfa..2a962a6 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -94,7 +94,7 @@ public class CompactUi : WindowMediatorSubscriberBase IpcManager ipcManager, LightFinderService broadcastService, CharacterAnalyzer characterAnalyzer, - PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService, PairLedger pairLedger) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) + PlayerPerformanceConfigService playerPerformanceConfig, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService, PairLedger pairLedger, LightFinderScannerService lightFinderScannerService) : base(logger, mediator, "###LightlessSyncMainUI", performanceCollectorService) { _uiSharedService = uiShared; _configService = configService; @@ -114,44 +114,17 @@ public class CompactUi : WindowMediatorSubscriberBase _broadcastService = broadcastService; _pairLedger = pairLedger; _dalamudUtilService = dalamudUtilService; - _tabMenu = new TopTabMenu(Mediator, _apiController, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService); + _tabMenu = new TopTabMenu(Mediator, _apiController, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService, broadcastService, lightFinderScannerService); Mediator.Subscribe(this, msg => RegisterFocusCharacter(msg.Pair)); - AllowPinning = true; - AllowClickthrough = false; - TitleBarButtons = - [ - new TitleBarButton() - { - Icon = FontAwesomeIcon.Cog, - Click = (msg) => - { - Mediator.Publish(new UiToggleMessage(typeof(SettingsUi))); - }, - IconOffset = new(2,1), - ShowTooltip = () => - { - ImGui.BeginTooltip(); - ImGui.Text("Open Lightless Settings"); - ImGui.EndTooltip(); - } - }, - new TitleBarButton() - { - Icon = FontAwesomeIcon.Book, - Click = (msg) => - { - Mediator.Publish(new UiToggleMessage(typeof(EventViewerUI))); - }, - IconOffset = new(2,1), - ShowTooltip = () => - { - ImGui.BeginTooltip(); - ImGui.Text("Open Lightless Event Viewer"); - ImGui.EndTooltip(); - } - }, - ]; + WindowBuilder.For(this) + .AllowPinning(true) + .AllowClickthrough(false) + .SetSizeConstraints(new Vector2(375, 400), new Vector2(375, 2000)) + .AddFlags(ImGuiWindowFlags.NoDocking) + .AddTitleBarButton(FontAwesomeIcon.Cog, "Open Lightless Settings", () => Mediator.Publish(new UiToggleMessage(typeof(SettingsUi)))) + .AddTitleBarButton(FontAwesomeIcon.Book, "Open Lightless Event Viewer", () => Mediator.Publish(new UiToggleMessage(typeof(EventViewerUI)))) + .Apply(); _drawFolders = [.. DrawFolders]; @@ -172,13 +145,6 @@ public class CompactUi : WindowMediatorSubscriberBase Mediator.Subscribe(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); Mediator.Subscribe(this, (msg) => _drawFolders = DrawFolders.ToList()); - Flags |= ImGuiWindowFlags.NoDocking; - - SizeConstraints = new WindowSizeConstraints() - { - MinimumSize = new Vector2(375, 400), - MaximumSize = new Vector2(375, 2000), - }; _characterAnalyzer = characterAnalyzer; _playerPerformanceConfig = playerPerformanceConfig; _lightlessMediator = mediator; @@ -534,7 +500,8 @@ public class CompactUi : WindowMediatorSubscriberBase private void DrawUIDHeader() { - var uidText = GetUidText(); + var uidText = _apiController.ServerState.GetUidText(_apiController.DisplayName); + var uidColor = _apiController.ServerState.GetUidColor(); Vector4? vanityTextColor = null; Vector4? vanityGlowColor = null; @@ -667,7 +634,7 @@ public class CompactUi : WindowMediatorSubscriberBase } else { - ImGui.TextColored(GetUidColor(), uidText); + ImGui.TextColored(uidColor, uidText); } } @@ -754,7 +721,7 @@ public class CompactUi : WindowMediatorSubscriberBase } else { - ImGui.TextColored(GetUidColor(), _apiController.UID); + ImGui.TextColored(uidColor, _apiController.UID); } if (ImGui.IsItemHovered()) @@ -781,7 +748,7 @@ public class CompactUi : WindowMediatorSubscriberBase } else { - UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor()); + UiSharedService.ColorTextWrapped(_apiController.ServerState.GetServerError(_apiController.AuthFailureMessage), uidColor); } } @@ -1048,73 +1015,6 @@ public class CompactUi : WindowMediatorSubscriberBase return SortGroupEntries(entries, group); } - private string GetServerError() - { - return _apiController.ServerState switch - { - ServerState.Connecting => "Attempting to connect to the server.", - ServerState.Reconnecting => "Connection to server interrupted, attempting to reconnect to the server.", - ServerState.Disconnected => "You are currently disconnected from the Lightless Sync server.", - ServerState.Disconnecting => "Disconnecting from the server", - ServerState.Unauthorized => "Server Response: " + _apiController.AuthFailureMessage, - ServerState.Offline => "Your selected Lightless Sync server is currently offline.", - ServerState.VersionMisMatch => - "Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version.", - ServerState.RateLimited => "You are rate limited for (re)connecting too often. Disconnect, wait 10 minutes and try again.", - ServerState.Connected => string.Empty, - ServerState.NoSecretKey => "You have no secret key set for this current character. Open Settings -> Service Settings and set a secret key for the current character. You can reuse the same secret key for multiple characters.", - ServerState.MultiChara => "Your Character Configuration has multiple characters configured with same name and world. You will not be able to connect until you fix this issue. Remove the duplicates from the configuration in Settings -> Service Settings -> Character Management and reconnect manually after.", - ServerState.OAuthMisconfigured => "OAuth2 is enabled but not fully configured, verify in the Settings -> Service Settings that you have OAuth2 connected and, importantly, a UID assigned to your current character.", - ServerState.OAuthLoginTokenStale => "Your OAuth2 login token is stale and cannot be used to renew. Go to the Settings -> Service Settings and unlink then relink your OAuth2 configuration.", - ServerState.NoAutoLogon => "This character has automatic login into Lightless disabled. Press the connect button to connect to Lightless.", - _ => string.Empty - }; - } - - private Vector4 GetUidColor() - { - return _apiController.ServerState switch - { - ServerState.Connecting => UIColors.Get("LightlessYellow"), - ServerState.Reconnecting => UIColors.Get("DimRed"), - ServerState.Connected => UIColors.Get("LightlessPurple"), - ServerState.Disconnected => UIColors.Get("LightlessYellow"), - ServerState.Disconnecting => UIColors.Get("LightlessYellow"), - ServerState.Unauthorized => UIColors.Get("DimRed"), - ServerState.VersionMisMatch => UIColors.Get("DimRed"), - ServerState.Offline => UIColors.Get("DimRed"), - ServerState.RateLimited => UIColors.Get("LightlessYellow"), - ServerState.NoSecretKey => UIColors.Get("LightlessYellow"), - ServerState.MultiChara => UIColors.Get("LightlessYellow"), - ServerState.OAuthMisconfigured => UIColors.Get("DimRed"), - ServerState.OAuthLoginTokenStale => UIColors.Get("DimRed"), - ServerState.NoAutoLogon => UIColors.Get("LightlessYellow"), - _ => UIColors.Get("DimRed") - }; - } - - private string GetUidText() - { - return _apiController.ServerState switch - { - ServerState.Reconnecting => "Reconnecting", - ServerState.Connecting => "Connecting", - ServerState.Disconnected => "Disconnected", - ServerState.Disconnecting => "Disconnecting", - ServerState.Unauthorized => "Unauthorized", - ServerState.VersionMisMatch => "Version mismatch", - ServerState.Offline => "Unavailable", - ServerState.RateLimited => "Rate Limited", - ServerState.NoSecretKey => "No Secret Key", - ServerState.MultiChara => "Duplicate Characters", - ServerState.OAuthMisconfigured => "Misconfigured OAuth2", - ServerState.OAuthLoginTokenStale => "Stale OAuth2", - ServerState.NoAutoLogon => "Auto Login disabled", - ServerState.Connected => _apiController.DisplayName, - _ => string.Empty - }; - } - private void UiSharedService_GposeEnd() { IsOpen = _wasOpen; diff --git a/LightlessSync/UI/CreateSyncshellUI.cs b/LightlessSync/UI/CreateSyncshellUI.cs index 215156b..2198a42 100644 --- a/LightlessSync/UI/CreateSyncshellUI.cs +++ b/LightlessSync/UI/CreateSyncshellUI.cs @@ -5,6 +5,7 @@ using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using System.Numerics; @@ -24,13 +25,10 @@ public class CreateSyncshellUI : WindowMediatorSubscriberBase { _apiController = apiController; _uiSharedService = uiSharedService; - SizeConstraints = new() - { - MinimumSize = new(550, 330), - MaximumSize = new(550, 330) - }; - - Flags = ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse; + WindowBuilder.For(this) + .SetFixedSize(new Vector2(550, 330)) + .AddFlags(ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse) + .Apply(); Mediator.Subscribe(this, (_) => IsOpen = false); } diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index 932653d..94c8add 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -110,19 +110,9 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { _hasUpdate = true; }); - SizeConstraints = new() - { - MinimumSize = new() - { - X = 1650, - Y = 1000 - }, - MaximumSize = new() - { - X = 3840, - Y = 2160 - } - }; + WindowBuilder.For(this) + .SetSizeConstraints(new Vector2(1650, 1000), new Vector2(3840, 2160)) + .Apply(); _conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged; } @@ -811,7 +801,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase var category = TextureMetadataHelper.DetermineCategory(classificationPath); var slot = TextureMetadataHelper.DetermineSlot(category, classificationPath); var format = entry.Format.Value; - var suggestion = TextureMetadataHelper.GetSuggestedTarget(format, mapKind); + var suggestion = TextureMetadataHelper.GetSuggestedTarget(format, mapKind, classificationPath); TextureCompressionTarget? currentTarget = TextureMetadataHelper.TryMapFormatToTarget(format, out var mappedTarget) ? mappedTarget : null; @@ -2131,8 +2121,16 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase }); DrawSelectableColumn(isSelected, () => { + Action? tooltipAction = null; ImGui.TextUnformatted(row.Format); - return null; + if (!row.IsAlreadyCompressed) + { + ImGui.SameLine(0f, 4f * ImGuiHelpers.GlobalScale); + var iconColor = isSelected ? SelectedTextureRowTextColor : UIColors.Get("LightlessYellow"); + _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, iconColor); + tooltipAction = () => UiSharedService.AttachToolTip("Run compression to reduce file size."); + } + return tooltipAction; }); DrawSelectableColumn(isSelected, () => diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 4682321..53d6b9e 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -52,7 +52,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase "bmp" }; private const string _imageFileDialogFilter = "Images{.png,.jpg,.jpeg,.webp,.bmp}"; - private readonly List _tagEditorSelection = []; + private readonly List _tagEditorSelection; private int[] _profileTagIds = []; private readonly List _tagPreviewSegments = new(); private enum ProfileEditorMode @@ -120,6 +120,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase _fileDialogManager = fileDialogManager; _lightlessProfileManager = lightlessProfileManager; _profileTagService = profileTagService; + _tagEditorSelection = new List(_maxProfileTags); Mediator.Subscribe(this, (_) => { _wasOpen = IsOpen; IsOpen = false; }); Mediator.Subscribe(this, (_) => IsOpen = _wasOpen); @@ -346,8 +347,8 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase SyncProfileState(profile); DrawSection("Profile Preview", scale, () => DrawProfileSnapshot(profile, scale)); - DrawSection("Profile Image", scale, () => DrawProfileImageControls(profile, scale)); - DrawSection("Profile Banner", scale, () => DrawProfileBannerControls(profile, scale)); + DrawSection("Profile Image", scale, () => DrawProfileImageControls(profile)); + DrawSection("Profile Banner", scale, () => DrawProfileBannerControls(profile)); DrawSection("Profile Description", scale, () => DrawProfileDescriptionEditor(profile, scale)); DrawSection("Profile Tags", scale, () => DrawProfileTagsEditor(profile, scale)); DrawSection("Visibility", scale, () => DrawProfileVisibilityControls()); @@ -434,7 +435,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase } - private void DrawProfileImageControls(LightlessUserProfileData profile, float scale) + private void DrawProfileImageControls(LightlessUserProfileData profile) { _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "Profile pictures must be 512x512 and under 2 MiB."); @@ -498,7 +499,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase } } - private void DrawProfileBannerControls(LightlessUserProfileData profile, float scale) + private void DrawProfileBannerControls(LightlessUserProfileData profile) { _uiSharedService.DrawNoteLine("# ", UIColors.Get("LightlessBlue"), "Profile banners must be 840x260 and under 2 MiB."); @@ -950,21 +951,6 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase ProfileTagRenderer.RenderTag(tag, rectMin, scale, drawList, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _tagPreviewSegments, ResolveIconWrap, _logger); } - private void DrawInfoCell(string displayName, string idLabel, float rowHeight, ImGuiStylePtr style) - { - var cellStart = ImGui.GetCursorPos(); - var nameSize = ImGui.CalcTextSize(displayName); - var idSize = ImGui.CalcTextSize(idLabel); - var totalHeight = nameSize.Y + style.ItemSpacing.Y + idSize.Y; - var offsetY = MathF.Max(0f, (rowHeight - totalHeight) * 0.5f) - style.CellPadding.Y; - if (offsetY < 0f) offsetY = 0f; - - ImGui.SetCursorPos(new Vector2(cellStart.X + style.CellPadding.X, cellStart.Y + offsetY)); - ImGui.TextUnformatted(displayName); - ImGui.SetCursorPos(new Vector2(cellStart.X + style.CellPadding.X, ImGui.GetCursorPosY() + style.ItemSpacing.Y)); - ImGui.TextDisabled(idLabel); - } - private void DrawReorderCell( string contextPrefix, int tagId, @@ -1002,8 +988,6 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase var cellStart = ImGui.GetCursorPos(); var available = ImGui.GetContentRegionAvail(); var buttonHeight = MathF.Max(1f, rowHeight - style.CellPadding.Y * 2f); - var hovered = BlendTowardsWhite(baseColor, 0.15f); - var active = BlendTowardsWhite(baseColor, 0.3f); ImGui.SetCursorPos(new Vector2(cellStart.X, cellStart.Y + style.CellPadding.Y)); using (ImRaii.PushId(idSuffix)) @@ -1013,7 +997,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase } } - private bool ColoredButton(string label, Vector4 baseColor, Vector2 size, float scale, bool disabled) + private static bool ColoredButton(string label, Vector4 baseColor, Vector2 size, float scale, bool disabled) { var style = ImGui.GetStyle(); var hovered = BlendTowardsWhite(baseColor, 0.15f); diff --git a/LightlessSync/UI/EventViewerUI.cs b/LightlessSync/UI/EventViewerUI.cs index 7afa1f7..17bcbb2 100644 --- a/LightlessSync/UI/EventViewerUI.cs +++ b/LightlessSync/UI/EventViewerUI.cs @@ -5,6 +5,7 @@ using Dalamud.Interface.Utility.Raii; using LightlessSync.Services; using LightlessSync.Services.Events; using LightlessSync.Services.Mediator; +using LightlessSync.Utils; using Microsoft.Extensions.Logging; using System.Diagnostics; using System.Globalization; @@ -43,11 +44,9 @@ internal class EventViewerUI : WindowMediatorSubscriberBase { _eventAggregator = eventAggregator; _uiSharedService = uiSharedService; - SizeConstraints = new() - { - MinimumSize = new(600, 500), - MaximumSize = new(1000, 2000) - }; + WindowBuilder.For(this) + .SetSizeConstraints(new Vector2(600, 500), new Vector2(1000, 2000)) + .Apply(); _filteredEvents = RecreateFilter(); } diff --git a/LightlessSync/UI/IntroUI.cs b/LightlessSync/UI/IntroUI.cs index 97935c2..4fab7ef 100644 --- a/LightlessSync/UI/IntroUI.cs +++ b/LightlessSync/UI/IntroUI.cs @@ -10,6 +10,7 @@ using LightlessSync.Localization; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; +using LightlessSync.Utils; using Microsoft.Extensions.Logging; using System.Numerics; using System.Text.RegularExpressions; @@ -46,11 +47,9 @@ public partial class IntroUi : WindowMediatorSubscriberBase ShowCloseButton = false; RespectCloseHotkey = false; - SizeConstraints = new WindowSizeConstraints() - { - MinimumSize = new Vector2(600, 400), - MaximumSize = new Vector2(600, 2000), - }; + WindowBuilder.For(this) + .SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 2000)) + .Apply(); GetToSLocalization(); diff --git a/LightlessSync/UI/LightFinderUI.cs b/LightlessSync/UI/LightFinderUI.cs index 9f118a3..fa88475 100644 --- a/LightlessSync/UI/LightFinderUI.cs +++ b/LightlessSync/UI/LightFinderUI.cs @@ -2,7 +2,6 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; -using Dalamud.Utility; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; using LightlessSync.LightlessConfiguration; @@ -47,11 +46,9 @@ namespace LightlessSync.UI _broadcastScannerService = broadcastScannerService; IsOpen = false; - this.SizeConstraints = new() - { - MinimumSize = new(600, 465), - MaximumSize = new(750, 525) - }; + WindowBuilder.For(this) + .SetSizeConstraints(new Vector2(600, 465), new Vector2(750, 525)) + .Apply(); } private void RebuildSyncshellDropdownOptions() diff --git a/LightlessSync/UI/PermissionWindowUI.cs b/LightlessSync/UI/PermissionWindowUI.cs index 45be154..5dee098 100644 --- a/LightlessSync/UI/PermissionWindowUI.cs +++ b/LightlessSync/UI/PermissionWindowUI.cs @@ -9,6 +9,7 @@ using LightlessSync.Services.Mediator; using LightlessSync.Utils; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; +using System.Numerics; namespace LightlessSync.UI; @@ -28,12 +29,10 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase _uiSharedService = uiSharedService; _apiController = apiController; _ownPermissions = pair.UserPair.OwnPermissions.DeepClone(); - Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize; - SizeConstraints = new() - { - MinimumSize = new(450, 100), - MaximumSize = new(450, 500) - }; + WindowBuilder.For(this) + .SetSizeConstraints(new Vector2(450, 100), new Vector2(450, 500)) + .AddFlags(ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize) + .Apply(); IsOpen = true; } diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 9602f5a..4ce64ac 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -1,5 +1,4 @@ using Dalamud.Bindings.ImGui; -using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Text; using Dalamud.Interface; using Dalamud.Interface.Colors; @@ -8,6 +7,7 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using LightlessSync.API.Data; using LightlessSync.API.Data.Comparer; +using LightlessSync.API.Data.Enum; using LightlessSync.API.Routes; using LightlessSync.FileCache; using LightlessSync.Interop.Ipc; @@ -18,9 +18,11 @@ using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.ActorTracking; +using LightlessSync.Services.Events; using LightlessSync.Services.Mediator; using LightlessSync.Services.PairProcessing; using LightlessSync.Services.ServerConfiguration; +using LightlessSync.UI.Models; using LightlessSync.UI.Services; using LightlessSync.UI.Style; using LightlessSync.Utils; @@ -39,7 +41,6 @@ using System.Net.Http.Json; using System.Numerics; using System.Text; using System.Text.Json; -using static Penumbra.GameData.Files.ShpkFile; namespace LightlessSync.UI; @@ -62,6 +63,7 @@ public class SettingsUi : WindowMediatorSubscriberBase private readonly PerformanceCollectorService _performanceCollector; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly PairProcessingLimiter _pairProcessingLimiter; + private readonly EventAggregator _eventAggregator; private readonly ServerConfigurationManager _serverConfigurationManager; private readonly UiSharedService _uiShared; private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress; @@ -75,11 +77,13 @@ public class SettingsUi : WindowMediatorSubscriberBase private bool _readClearCache = false; private int _selectedEntry = -1; private string _uidToAddForIgnore = string.Empty; - private string _lightfinderIconInput = string.Empty; - private bool _lightfinderIconInputInitialized = false; - private int _lightfinderIconPresetIndex = -1; private bool _selectGeneralTabOnNextDraw = false; + private string _pairDebugFilter = string.Empty; + private bool _pairDebugVisibleOnly = true; + private bool _pairDiagnosticsEnabled; + private string? _selectedPairDebugUid = null; private static readonly LightlessConfig DefaultConfig = new(); + private static readonly JsonSerializerOptions DebugJsonOptions = new() { WriteIndented = true }; private MainSettingsTab _selectedMainTab = MainSettingsTab.General; private TransferSettingsTab _selectedTransferTab = TransferSettingsTab.Transfers; private ServerSettingsTab _selectedServerTab = ServerSettingsTab.CharacterManagement; @@ -143,15 +147,6 @@ public class SettingsUi : WindowMediatorSubscriberBase PermissionSettings, } - private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[] - { - ("Link Marker", SeIconChar.LinkMarker), ("Hyadelyn", SeIconChar.Hyadelyn), ("Gil", SeIconChar.Gil), - ("Quest Sync", SeIconChar.QuestSync), ("Glamoured", SeIconChar.Glamoured), - ("Glamoured (Dyed)", SeIconChar.GlamouredDyed), ("Auto-Translate Open", SeIconChar.AutoTranslateOpen), - ("Auto-Translate Close", SeIconChar.AutoTranslateClose), ("Boxed Star", SeIconChar.BoxedStar), - ("Boxed Plus", SeIconChar.BoxedPlus) - }; - private CancellationTokenSource? _validationCts; private Task>? _validationTask; private bool _wasOpen = false; @@ -162,6 +157,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ServerConfigurationManager serverConfigurationManager, PlayerPerformanceConfigService playerPerformanceConfigService, PairProcessingLimiter pairProcessingLimiter, + EventAggregator eventAggregator, LightlessMediator mediator, PerformanceCollectorService performanceCollector, FileUploadManager fileTransferManager, FileTransferOrchestrator fileTransferOrchestrator, @@ -179,6 +175,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _serverConfigurationManager = serverConfigurationManager; _playerPerformanceConfigService = playerPerformanceConfigService; _pairProcessingLimiter = pairProcessingLimiter; + _eventAggregator = eventAggregator; _performanceCollector = performanceCollector; _fileTransferManager = fileTransferManager; _fileTransferOrchestrator = fileTransferOrchestrator; @@ -192,34 +189,14 @@ public class SettingsUi : WindowMediatorSubscriberBase _uiShared = uiShared; _nameplateService = nameplateService; _actorObjectService = actorObjectService; - AllowClickthrough = false; - AllowPinning = true; _validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v); - SizeConstraints = new WindowSizeConstraints() - { - MinimumSize = new Vector2(900f, 400f), - MaximumSize = new Vector2(900f, 2000f), - }; - - TitleBarButtons = new() - { - new TitleBarButton() - { - Icon = FontAwesomeIcon.FileAlt, - Click = (msg) => - { - Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi))); - }, - IconOffset = new(2, 1), - ShowTooltip = () => - { - ImGui.BeginTooltip(); - ImGui.Text("View Update Notes"); - ImGui.EndTooltip(); - } - } - }; + WindowBuilder.For(this) + .AllowPinning(true) + .AllowClickthrough(false) + .SetSizeConstraints(new Vector2(900f, 400f), new Vector2(900f, 2000f)) + .AddTitleBarButton(FontAwesomeIcon.FileAlt, "View Update Notes", () => Mediator.Publish(new UiToggleMessage(typeof(UpdateNotesUi)))) + .Apply(); Mediator.Subscribe(this, (_) => Toggle()); Mediator.Subscribe(this, (_) => @@ -1309,9 +1286,425 @@ public class SettingsUi : WindowMediatorSubscriberBase UiSharedService.TooltipSeparator + "Keeping LOD enabled can lead to more crashes. Use at your own risk."); + ImGuiHelpers.ScaledDummy(10f); + DrawPairDebugPanel(); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 2f); } + private void DrawPairDebugPanel() + { + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow")); + ImGui.TextUnformatted("Pair Diagnostics"); + ImGui.PopStyleColor(); + ImGuiHelpers.ScaledDummy(3f); + + ImGui.Checkbox("Enable Pair Diagnostics", ref _pairDiagnosticsEnabled); + UiSharedService.AttachToolTip("When disabled the UI stops querying pair handlers and no diagnostics are processed."); + + if (!_pairDiagnosticsEnabled) + { + UiSharedService.ColorTextWrapped("Diagnostics are disabled. Enable the toggle above to inspect active pairs.", UIColors.Get("LightlessYellow")); + return; + } + + var snapshot = _pairUiService.GetSnapshot(); + if (snapshot.PairsByUid.Count == 0) + { + UiSharedService.ColorTextWrapped("No pairs are currently tracked. Connect to the service and re-open this panel.", UIColors.Get("LightlessYellow")); + return; + } + + ImGui.SetNextItemWidth(280f * ImGuiHelpers.GlobalScale); + ImGui.InputTextWithHint("##pairDebugFilter", "Search by UID, alias, or player name...", ref _pairDebugFilter, 96); + UiSharedService.AttachToolTip("Filters the list by UID, aliases, or currently cached player name."); + ImGui.SameLine(); + ImGui.Checkbox("Visible pairs only", ref _pairDebugVisibleOnly); + UiSharedService.AttachToolTip("When enabled only currently visible pairs remain in the list."); + + var pairs = snapshot.PairsByUid.Values; + var filteredPairs = pairs + .Where(p => !_pairDebugVisibleOnly || p.IsVisible) + .Where(p => PairMatchesFilter(p, _pairDebugFilter)) + .OrderByDescending(p => p.IsVisible) + .ThenByDescending(p => p.IsOnline) + .ThenBy(p => p.UserData.AliasOrUID, StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (filteredPairs.Count == 0) + { + UiSharedService.ColorTextWrapped("No pairs match the current filters.", UIColors.Get("LightlessYellow")); + _selectedPairDebugUid = null; + return; + } + + if (_selectedPairDebugUid is null || !filteredPairs.Any(p => string.Equals(p.UserData.UID, _selectedPairDebugUid, StringComparison.Ordinal))) + { + _selectedPairDebugUid = filteredPairs[0].UserData.UID; + } + + if (_selectedPairDebugUid is null || !snapshot.PairsByUid.TryGetValue(_selectedPairDebugUid, out var selectedPair)) + { + selectedPair = filteredPairs[0]; + } + + var visibleCount = pairs.Count(p => p.IsVisible); + var onlineCount = pairs.Count(p => p.IsOnline); + var totalPairs = snapshot.PairsByUid.Count; + ImGui.TextUnformatted($"Visible: {visibleCount} / {totalPairs}; Online: {onlineCount}"); + + var mainChildHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * 12f, ImGui.GetContentRegionAvail().Y * 0.95f); + if (ImGui.BeginChild("##pairDebugPanel", new Vector2(-1, mainChildHeight), true, ImGuiWindowFlags.HorizontalScrollbar)) + { + var childAvail = ImGui.GetContentRegionAvail(); + var leftWidth = MathF.Max(220f * ImGuiHelpers.GlobalScale, childAvail.X * 0.35f); + leftWidth = MathF.Min(leftWidth, childAvail.X * 0.6f); + if (ImGui.BeginChild("##pairDebugList", new Vector2(leftWidth, 0), true, ImGuiWindowFlags.HorizontalScrollbar)) + { + if (ImGui.BeginTable("##pairDebugTable", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollY | ImGuiTableFlags.ScrollX)) + { + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 20f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Pair"); + ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 90f * ImGuiHelpers.GlobalScale); + ImGui.TableHeadersRow(); + + foreach (var entry in filteredPairs) + { + var isSelected = string.Equals(entry.UserData.UID, _selectedPairDebugUid, StringComparison.Ordinal); + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + DrawPairStateIndicator(entry); + + ImGui.TableNextColumn(); + if (ImGui.Selectable($"{entry.UserData.AliasOrUID}##pairDebugSelect_{entry.UserData.UID}", isSelected, ImGuiSelectableFlags.SpanAllColumns)) + { + _selectedPairDebugUid = entry.UserData.UID; + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip($"UID: {entry.UserData.UID}\nVisible: {entry.IsVisible}\nOnline: {entry.IsOnline}\nDirect pair: {entry.IsDirectlyPaired}"); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted(entry.IsVisible ? "Visible" : entry.IsOnline ? "Online" : "Offline"); + } + + ImGui.EndTable(); + } + } + ImGui.EndChild(); + + ImGui.SameLine(); + + if (ImGui.BeginChild("##pairDebugDetails", new Vector2(0, 0), true, ImGuiWindowFlags.HorizontalScrollbar)) + { + DrawPairDebugDetails(selectedPair, snapshot); + } + ImGui.EndChild(); + } + ImGui.EndChild(); + } + + private static bool PairMatchesFilter(Pair pair, string filter) + { + if (string.IsNullOrWhiteSpace(filter)) + { + return true; + } + + return pair.UserData.UID.Contains(filter, StringComparison.OrdinalIgnoreCase) + || pair.UserData.AliasOrUID.Contains(filter, StringComparison.OrdinalIgnoreCase) + || (!string.IsNullOrEmpty(pair.PlayerName) && pair.PlayerName.Contains(filter, StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrEmpty(pair.Ident) && pair.Ident.Contains(filter, StringComparison.OrdinalIgnoreCase)); + } + + private static void DrawPairStateIndicator(Pair pair) + { + var color = pair.IsVisible + ? UIColors.Get("LightlessGreen") + : pair.IsOnline ? UIColors.Get("LightlessYellow") + : UIColors.Get("DimRed"); + + var drawList = ImGui.GetWindowDrawList(); + var cursor = ImGui.GetCursorScreenPos(); + var radius = ImGui.GetTextLineHeight() * 0.35f; + var center = cursor + new Vector2(radius, radius); + drawList.AddCircleFilled(center, radius, ImGui.ColorConvertFloat4ToU32(color)); + ImGui.Dummy(new Vector2(radius * 2f, radius * 2f)); + } + + private void DrawPairDebugDetails(Pair pair, PairUiSnapshot snapshot) + { + var debugInfo = pair.GetDebugInfo(); + var statusColor = pair.IsVisible + ? UIColors.Get("LightlessGreen") + : pair.IsOnline ? UIColors.Get("LightlessYellow") + : UIColors.Get("DimRed"); + + ImGui.TextColored(statusColor, pair.UserData.AliasOrUID); + ImGui.SameLine(); + ImGui.TextColored(statusColor, $"[{(pair.IsVisible ? "Visible" : pair.IsOnline ? "Online" : "Offline")}]"); + + if (ImGui.BeginTable("##pairDebugProperties", 2, ImGuiTableFlags.SizingStretchProp)) + { + DrawPairPropertyRow("UID", pair.UserData.UID); + DrawPairPropertyRow("Alias", string.IsNullOrEmpty(pair.UserData.Alias) ? "(none)" : pair.UserData.Alias!); + DrawPairPropertyRow("Player Name", pair.PlayerName ?? "(not cached)"); + DrawPairPropertyRow("Handler Ident", string.IsNullOrEmpty(pair.Ident) ? "(not bound)" : pair.Ident); + DrawPairPropertyRow("Character Id", FormatCharacterId(pair.PlayerCharacterId)); + DrawPairPropertyRow("Direct Pair", FormatBool(pair.IsDirectlyPaired)); + DrawPairPropertyRow("Individual Status", pair.IndividualPairStatus.ToString()); + DrawPairPropertyRow("Any Connection", FormatBool(pair.HasAnyConnection())); + DrawPairPropertyRow("Paused", FormatBool(pair.IsPaused)); + DrawPairPropertyRow("Visible", FormatBool(pair.IsVisible), statusColor); + DrawPairPropertyRow("Online", FormatBool(pair.IsOnline)); + DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler)); + DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized)); + DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible)); + DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion)); + DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)"); + ImGui.EndTable(); + } + + ImGui.Separator(); + ImGui.TextUnformatted("Applied Data"); + if (ImGui.BeginTable("##pairDebugDataStats", 2, ImGuiTableFlags.SizingStretchProp)) + { + DrawPairPropertyRow("Last Data Size", FormatBytes(pair.LastAppliedDataBytes)); + DrawPairPropertyRow("Approx. VRAM", FormatBytes(pair.LastAppliedApproximateVRAMBytes)); + DrawPairPropertyRow("Effective VRAM", FormatBytes(pair.LastAppliedApproximateEffectiveVRAMBytes)); + DrawPairPropertyRow("Last Triangles", pair.LastAppliedDataTris < 0 ? "n/a" : pair.LastAppliedDataTris.ToString(CultureInfo.InvariantCulture)); + ImGui.EndTable(); + } + + var lastData = pair.LastReceivedCharacterData; + if (lastData is null) + { + ImGui.TextDisabled("No character data has been received for this pair."); + } + else + { + var fileReplacementCount = lastData.FileReplacements.Values.Sum(list => list?.Count ?? 0); + var totalGamePaths = lastData.FileReplacements.Values.Sum(list => list?.Sum(replacement => replacement.GamePaths.Length) ?? 0); + ImGui.BulletText($"File replacements: {fileReplacementCount} entries across {totalGamePaths} game paths."); + ImGui.BulletText($"Customize+: {lastData.CustomizePlusData.Count}, Glamourer entries: {lastData.GlamourerData.Count}"); + ImGui.BulletText($"Manipulation length: {lastData.ManipulationData.Length}, Heels set: {FormatBool(!string.IsNullOrEmpty(lastData.HeelsData))}"); + + if (ImGui.TreeNode("Last Received Character Data (JSON)")) + { + DrawJsonBlob(lastData); + ImGui.TreePop(); + } + } + + ImGui.Separator(); + ImGui.TextUnformatted("Application Timeline"); + if (ImGui.BeginTable("##pairDebugTimeline", 2, ImGuiTableFlags.SizingStretchProp)) + { + DrawPairPropertyRow("Last Data Received", FormatTimestamp(debugInfo.LastDataReceivedAt)); + DrawPairPropertyRow("Last Apply Attempt", FormatTimestamp(debugInfo.LastApplyAttemptAt)); + DrawPairPropertyRow("Last Successful Apply", FormatTimestamp(debugInfo.LastSuccessfulApplyAt)); + ImGui.EndTable(); + } + + if (!string.IsNullOrEmpty(debugInfo.LastFailureReason)) + { + UiSharedService.ColorTextWrapped($"Last failure: {debugInfo.LastFailureReason}", UIColors.Get("DimRed")); + if (debugInfo.BlockingConditions.Count > 0) + { + ImGui.TextUnformatted("Blocking conditions:"); + foreach (var condition in debugInfo.BlockingConditions) + { + ImGui.BulletText(condition); + } + } + } + + ImGui.Separator(); + ImGui.TextUnformatted("Application & Download State"); + if (ImGui.BeginTable("##pairDebugProcessing", 2, ImGuiTableFlags.SizingStretchProp)) + { + DrawPairPropertyRow("Applying Data", FormatBool(debugInfo.IsApplying)); + DrawPairPropertyRow("Downloading", FormatBool(debugInfo.IsDownloading)); + DrawPairPropertyRow("Pending Downloads", debugInfo.PendingDownloadCount.ToString(CultureInfo.InvariantCulture)); + DrawPairPropertyRow("Forbidden Downloads", debugInfo.ForbiddenDownloadCount.ToString(CultureInfo.InvariantCulture)); + ImGui.EndTable(); + } + + ImGui.Separator(); + ImGui.TextUnformatted("Syncshell Memberships"); + if (snapshot.PairsWithGroups.TryGetValue(pair, out var groups) && groups.Count > 0) + { + foreach (var group in groups.OrderBy(g => g.Group.AliasOrGID, StringComparer.OrdinalIgnoreCase)) + { + var flags = group.GroupPairUserInfos.TryGetValue(pair.UserData.UID, out var info) ? info : GroupPairUserInfo.None; + var flagLabel = flags switch + { + GroupPairUserInfo.None => string.Empty, + _ => $" ({string.Join(", ", GetGroupInfoFlags(flags))})" + }; + ImGui.BulletText($"{group.Group.AliasOrGID} [{group.Group.GID}]{flagLabel}"); + } + } + else + { + ImGui.TextDisabled("Not a member of any syncshells."); + } + + if (pair.UserPair is null) + { + ImGui.TextDisabled("Pair DTO snapshot unavailable."); + } + else if (ImGui.TreeNode("Pair DTO Snapshot")) + { + DrawJsonBlob(pair.UserPair); + ImGui.TreePop(); + } + + ImGui.Separator(); + DrawPairEventLog(pair); + } + + private static IEnumerable GetGroupInfoFlags(GroupPairUserInfo info) + { + if (info.HasFlag(GroupPairUserInfo.IsModerator)) + { + yield return "Moderator"; + } + + if (info.HasFlag(GroupPairUserInfo.IsPinned)) + { + yield return "Pinned"; + } + } + + private void DrawPairEventLog(Pair pair) + { + ImGui.TextUnformatted("Recent Events"); + var events = _eventAggregator.EventList.Value; + var alias = pair.UserData.Alias; + var aliasOrUid = pair.UserData.AliasOrUID; + var rawUid = pair.UserData.UID; + var playerName = pair.PlayerName; + + var relevantEvents = events.Where(e => + EventMatchesIdentifier(e, rawUid) + || EventMatchesIdentifier(e, aliasOrUid) + || EventMatchesIdentifier(e, alias) + || (!string.IsNullOrEmpty(playerName) && string.Equals(e.Character, playerName, StringComparison.OrdinalIgnoreCase))) + .OrderByDescending(e => e.EventTime) + .Take(40) + .ToList(); + + if (relevantEvents.Count == 0) + { + ImGui.TextDisabled("No recent events were logged for this pair."); + return; + } + + var baseTableHeight = 300f * ImGuiHelpers.GlobalScale; + var tableHeight = MathF.Max(baseTableHeight, ImGui.GetContentRegionAvail().Y); + if (ImGui.BeginTable("##pairDebugEvents", 3, + ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.ScrollX | ImGuiTableFlags.ScrollY, + new Vector2(0f, tableHeight))) + { + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("Time", ImGuiTableColumnFlags.WidthFixed, 110f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, 60f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Details"); + ImGui.TableHeadersRow(); + + foreach (var ev in relevantEvents) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(ev.EventTime.ToString("T", CultureInfo.CurrentCulture)); + + ImGui.TableNextColumn(); + var (icon, color) = ev.EventSeverity switch + { + EventSeverity.Informational => (FontAwesomeIcon.InfoCircle, UIColors.Get("LightlessGreen")), + EventSeverity.Warning => (FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow")), + EventSeverity.Error => (FontAwesomeIcon.ExclamationCircle, UIColors.Get("DimRed")), + _ => (FontAwesomeIcon.QuestionCircle, UIColors.Get("LightlessGrey")) + }; + _uiShared.IconText(icon, color); + UiSharedService.AttachToolTip(ev.EventSeverity.ToString()); + + ImGui.TableNextColumn(); + ImGui.TextWrapped($"[{ev.EventSource}] {ev.Message}"); + } + + ImGui.EndTable(); + } + } + + private static bool EventMatchesIdentifier(Event evt, string? identifier) + { + if (string.IsNullOrWhiteSpace(identifier)) + { + return false; + } + + return (!string.IsNullOrEmpty(evt.UserId) && string.Equals(evt.UserId, identifier, StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrEmpty(evt.AliasOrUid) && string.Equals(evt.AliasOrUid, identifier, StringComparison.OrdinalIgnoreCase)) + || (!string.IsNullOrEmpty(evt.UID) && string.Equals(evt.UID, identifier, StringComparison.OrdinalIgnoreCase)); + } + + private static void DrawJsonBlob(object? value) + { + if (value is null) + { + ImGui.TextDisabled("(null)"); + return; + } + + try + { + var json = JsonSerializer.Serialize(value, DebugJsonOptions); + ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessGrey")); + foreach (var line in json.Split('\n')) + { + ImGui.TextUnformatted(line); + } + ImGui.PopStyleColor(); + } + catch (Exception ex) + { + UiSharedService.ColorTextWrapped($"Failed to serialize data: {ex.Message}", UIColors.Get("DimRed")); + } + } + + private static void DrawPairPropertyRow(string label, string value, Vector4? colorOverride = null) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(label); + ImGui.TableNextColumn(); + if (colorOverride is { } color) + { + ImGui.TextColored(color, value); + } + else + { + ImGui.TextUnformatted(value); + } + } + + private static string FormatTimestamp(DateTime? value) + { + return value is null ? "n/a" : value.Value.ToLocalTime().ToString("G", CultureInfo.CurrentCulture); + } + + private static string FormatBytes(long value) => value < 0 ? "n/a" : UiSharedService.ByteToString(value); + + private static string FormatCharacterId(uint id) => id == uint.MaxValue ? "n/a" : $"{id} (0x{id:X8})"; + + private static string FormatBool(bool value) => value ? "Yes" : "No"; + + private void DrawFileStorageSettings() { _lastTab = "FileCache"; @@ -2092,11 +2485,6 @@ public class SettingsUi : WindowMediatorSubscriberBase { // redo } - else - { - _lightfinderIconInputInitialized = false; - _lightfinderIconPresetIndex = -1; - } } _uiShared.DrawHelpText("Switch between the Lightfinder text label and an icon on nameplates."); @@ -2105,11 +2493,6 @@ public class SettingsUi : WindowMediatorSubscriberBase { //redo } - else - { - _lightfinderIconInputInitialized = false; - _lightfinderIconPresetIndex = -1; - } UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); diff --git a/LightlessSync/UI/StandaloneProfileUi.cs b/LightlessSync/UI/StandaloneProfileUi.cs index eb694aa..684caef 100644 --- a/LightlessSync/UI/StandaloneProfileUi.cs +++ b/LightlessSync/UI/StandaloneProfileUi.cs @@ -42,6 +42,8 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase private const float DescriptionMaxVisibleLines = 12f; private const string UserDescriptionPlaceholder = "-- User has no description set --"; private const string GroupDescriptionPlaceholder = "-- Syncshell has no description set --"; + private const string LightfinderDisplayName = "Lightfinder User"; + private readonly string _lightfinderDisplayName = LightfinderDisplayName; private float _lastComputedWindowHeight = -1f; public StandaloneProfileUi( @@ -50,6 +52,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase UiSharedService uiBuilder, ServerConfigurationManager serverManager, ProfileTagService profileTagService, + DalamudUtilService dalamudUtilService, LightlessProfileManager lightlessProfileManager, PairUiService pairUiService, Pair? pair, @@ -58,7 +61,12 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase bool isLightfinderContext, string? lightfinderCid, PerformanceCollectorService performanceCollector) - : base(logger, mediator, BuildWindowTitle(userData, groupData, isLightfinderContext), performanceCollector) + : base(logger, mediator, BuildWindowTitle( + userData, + groupData, + isLightfinderContext, + isLightfinderContext ? ResolveLightfinderDisplayName(dalamudUtilService, lightfinderCid) : null), + performanceCollector) { _uiSharedService = uiBuilder; _serverManager = serverManager; @@ -71,17 +79,19 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase _isGroupProfile = groupData is not null; _isLightfinderContext = isLightfinderContext; _lightfinderCid = lightfinderCid; + if (_isLightfinderContext) + _lightfinderDisplayName = ResolveLightfinderDisplayName(dalamudUtilService, lightfinderCid); Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize; var fixedSize = new Vector2(840f, 525f) * ImGuiHelpers.GlobalScale; Size = fixedSize; SizeCondition = ImGuiCond.Always; - SizeConstraints = new() - { - MinimumSize = fixedSize, - MaximumSize = new Vector2(fixedSize.X, fixedSize.Y * MaxHeightMultiplier) - }; + WindowBuilder.For(this) + .SetSizeConstraints( + fixedSize, + new Vector2(fixedSize.X, fixedSize.Y * MaxHeightMultiplier)) + .Apply(); IsOpen = true; } @@ -115,7 +125,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase return fallback; } - private static string BuildWindowTitle(UserData? userData, GroupData? groupData, bool isLightfinderContext) + private static string BuildWindowTitle(UserData? userData, GroupData? groupData, bool isLightfinderContext, string? lightfinderDisplayName) { if (groupData is not null) { @@ -126,11 +136,24 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase if (userData is null) return "Lightless Profile##LightlessSyncStandaloneProfileUI"; - var name = userData.AliasOrUID; + var name = isLightfinderContext ? lightfinderDisplayName ?? LightfinderDisplayName : userData.AliasOrUID; var suffix = isLightfinderContext ? " (Lightfinder)" : string.Empty; return $"Lightless Profile of {name}{suffix}##LightlessSyncStandaloneProfileUI{name}"; } + private static string ResolveLightfinderDisplayName(DalamudUtilService dalamudUtilService, string? hashedCid) + { + if (string.IsNullOrEmpty(hashedCid)) + return LightfinderDisplayName; + + var (name, address) = dalamudUtilService.FindPlayerByNameHash(hashedCid); + if (string.IsNullOrEmpty(name)) + return LightfinderDisplayName; + + var world = dalamudUtilService.GetWorldNameFromPlayerAddress(address); + return string.IsNullOrEmpty(world) ? name : $"{name} ({world})"; + } + protected override void DrawInternal() { try @@ -300,7 +323,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase ? Pair.UserData.Alias! : _isLightfinderContext ? "Lightfinder Session" : noteText ?? string.Empty; - bool hasVanityAlias = userData.HasVanity && !string.IsNullOrWhiteSpace(userData.Alias); + bool hasVanityAlias = !_isLightfinderContext && userData.HasVanity && !string.IsNullOrWhiteSpace(userData.Alias); Vector4? vanityTextColor = null; Vector4? vanityGlowColor = null; @@ -314,10 +337,12 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase } bool useVanityColors = vanityTextColor.HasValue || vanityGlowColor.HasValue; - string primaryHeaderText = hasVanityAlias ? userData.Alias! : userData.UID; + string primaryHeaderText = _isLightfinderContext + ? _lightfinderDisplayName + : hasVanityAlias ? userData.Alias! : userData.UID; List<(string Text, bool UseVanityColor, bool Disabled)> secondaryHeaderLines = new(); - if (hasVanityAlias) + if (!_isLightfinderContext && hasVanityAlias) { secondaryHeaderLines.Add((userData.UID, useVanityColors, false)); @@ -1232,4 +1257,4 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase bool Emphasis, IReadOnlyList? Tooltip = null, string? TooltipTitle = null); -} \ No newline at end of file +} diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 310d79e..7374203 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -60,11 +60,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase _dalamudUtilService = dalamudUtilService; IsOpen = false; - SizeConstraints = new() - { - MinimumSize = new(600, 400), - MaximumSize = new(600, 550) - }; + WindowBuilder.For(this) + .SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 550)) + .Apply(); Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); Mediator.Subscribe(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false)); diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index dabe8c0..16f3ea0 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -7,17 +7,14 @@ using Dalamud.Utility; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.LightlessConfiguration.Models; -using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Services.LightFinder; using LightlessSync.Utils; using LightlessSync.UI.Models; using LightlessSync.UI.Style; using LightlessSync.WebAPI; using System.Numerics; -using System.Threading.Tasks; -using System.Linq; - namespace LightlessSync.UI; @@ -29,6 +26,8 @@ public class TopTabMenu private readonly PairRequestService _pairRequestService; private readonly DalamudUtilService _dalamudUtilService; + private readonly LightFinderService _lightFinderService; + private readonly LightFinderScannerService _lightFinderScannerService; private readonly HashSet _pendingPairRequestActions = new(StringComparer.Ordinal); private bool _pairRequestsExpanded; // useless for now private int _lastRequestCount; @@ -42,7 +41,7 @@ public class TopTabMenu private SelectedTab _selectedTab = SelectedTab.None; private PairUiSnapshot? _currentSnapshot; - public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService) + public TopTabMenu(LightlessMediator lightlessMediator, ApiController apiController, UiSharedService uiSharedService, PairRequestService pairRequestService, DalamudUtilService dalamudUtilService, NotificationService lightlessNotificationService, LightFinderService lightFinderService, LightFinderScannerService lightFinderScannerService) { _lightlessMediator = lightlessMediator; _apiController = apiController; @@ -50,6 +49,8 @@ public class TopTabMenu _dalamudUtilService = dalamudUtilService; _uiSharedService = uiSharedService; _lightlessNotificationService = lightlessNotificationService; + _lightFinderService = lightFinderService; + _lightFinderScannerService = lightFinderScannerService; } private enum SelectedTab @@ -154,7 +155,7 @@ public class TopTabMenu Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding); } } - UiSharedService.AttachToolTip("Zone Chat"); + UiSharedService.AttachToolTip("Lightless Chat"); ImGui.SameLine(); ImGui.SameLine(); @@ -786,12 +787,28 @@ public class TopTabMenu ImGui.SameLine(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Globe, "Syncshell Finder", buttonX, center: true)) + var syncshellFinderLabel = GetSyncshellFinderLabel(); + if (_uiSharedService.IconTextButton(FontAwesomeIcon.Globe, syncshellFinderLabel, buttonX, center: true)) { _lightlessMediator.Publish(new UiToggleMessage(typeof(SyncshellFinderUI))); } } + private string GetSyncshellFinderLabel() + { + if (!_lightFinderService.IsBroadcasting) + return "Syncshell Finder"; + + var nearbyCount = _lightFinderScannerService + .GetActiveSyncshellBroadcasts() + .Where(b => !string.IsNullOrEmpty(b.GID)) + .Select(b => b.GID!) + .Distinct(StringComparer.Ordinal) + .Count(); + + return nearbyCount > 0 ? $"Syncshell Finder ({nearbyCount})" : "Syncshell Finder"; + } + private void DrawUserConfig(float availableWidth, float spacingX) { var buttonX = (availableWidth - spacingX) / 2f; diff --git a/LightlessSync/UI/UIColors.cs b/LightlessSync/UI/UIColors.cs index 90911d7..9d7f770 100644 --- a/LightlessSync/UI/UIColors.cs +++ b/LightlessSync/UI/UIColors.cs @@ -19,6 +19,7 @@ namespace LightlessSync.UI { "LightlessGreen", "#7cd68a" }, { "LightlessGreenDefault", "#468a50" }, { "LightlessOrange", "#ffb366" }, + { "LightlessGrey", "#8f8f8f" }, { "PairBlue", "#88a2db" }, { "DimRed", "#d44444" }, { "LightlessAdminText", "#ffd663" }, diff --git a/LightlessSync/UI/UpdateNotesUi.cs b/LightlessSync/UI/UpdateNotesUi.cs index 5fb2480..c5331a0 100644 --- a/LightlessSync/UI/UpdateNotesUi.cs +++ b/LightlessSync/UI/UpdateNotesUi.cs @@ -13,6 +13,7 @@ using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; using Dalamud.Interface; using LightlessSync.UI.Models; +using LightlessSync.Utils; namespace LightlessSync.UI; @@ -69,21 +70,20 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase _uiShared = uiShared; _configService = configService; - AllowClickthrough = false; - AllowPinning = false; RespectCloseHotkey = true; ShowCloseButton = true; Flags = ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove; - SizeConstraints = new WindowSizeConstraints() - { - MinimumSize = new Vector2(800, 700), MaximumSize = new Vector2(800, 700), - }; - PositionCondition = ImGuiCond.Always; + WindowBuilder.For(this) + .AllowPinning(false) + .AllowClickthrough(false) + .SetFixedSize(new Vector2(800, 700)) + .Apply(); + LoadEmbeddedResources(); logger.LogInformation("UpdateNotesUi constructor completed successfully"); } diff --git a/LightlessSync/UI/ZoneChatUi.cs b/LightlessSync/UI/ZoneChatUi.cs index 084b55b..93edc5d 100644 --- a/LightlessSync/UI/ZoneChatUi.cs +++ b/LightlessSync/UI/ZoneChatUi.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; using System.Numerics; -using System.Threading.Tasks; using LightlessSync.API.Data; using Dalamud.Bindings.ImGui; using Dalamud.Interface; @@ -13,7 +9,6 @@ using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Dto.Chat; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Models; -using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Chat; using LightlessSync.Services.Mediator; @@ -75,7 +70,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase ChatConfigService chatConfigService, ApiController apiController, PerformanceCollectorService performanceCollectorService) - : base(logger, mediator, "Zone Chat", performanceCollectorService) + : base(logger, mediator, "Lightless Chat", performanceCollectorService) { _uiSharedService = uiSharedService; _zoneChatService = zoneChatService; @@ -93,11 +88,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase RefreshWindowFlags(); Size = new Vector2(450, 420) * ImGuiHelpers.GlobalScale; SizeCondition = ImGuiCond.FirstUseEver; - SizeConstraints = new() - { - MinimumSize = new Vector2(320f, 260f) * ImGuiHelpers.GlobalScale, - MaximumSize = new Vector2(900f, 900f) * ImGuiHelpers.GlobalScale - }; + WindowBuilder.For(this) + .SetSizeConstraints( + new Vector2(320f, 260f) * ImGuiHelpers.GlobalScale, + new Vector2(900f, 900f) * ImGuiHelpers.GlobalScale) + .Apply(); Mediator.Subscribe(this, OnChatChannelMessageAdded); Mediator.Subscribe(this, msg => diff --git a/LightlessSync/Utils/WindowUtils.cs b/LightlessSync/Utils/WindowUtils.cs new file mode 100644 index 0000000..fb88a84 --- /dev/null +++ b/LightlessSync/Utils/WindowUtils.cs @@ -0,0 +1,139 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; +using LightlessSync.UI; +using LightlessSync.WebAPI.SignalR.Utils; +using System.Numerics; + +namespace LightlessSync.Utils; + +public sealed class WindowBuilder +{ + private readonly Window _window; + private readonly List _titleButtons = new(); + + private WindowBuilder(Window window) + { + _window = window ?? throw new ArgumentNullException(nameof(window)); + } + + public static WindowBuilder For(Window window) => new(window); + + public WindowBuilder AllowPinning(bool allow = true) + { + _window.AllowPinning = allow; + return this; + } + + public WindowBuilder AllowClickthrough(bool allow = true) + { + _window.AllowClickthrough = allow; + return this; + } + + public WindowBuilder SetFixedSize(Vector2 size) => SetSizeConstraints(size, size); + + public WindowBuilder SetSizeConstraints(Vector2 min, Vector2 max) + { + _window.SizeConstraints = new Window.WindowSizeConstraints + { + MinimumSize = min, + MaximumSize = max, + }; + return this; + } + + public WindowBuilder AddFlags(ImGuiWindowFlags flags) + { + _window.Flags |= flags; + return this; + } + + public WindowBuilder AddTitleBarButton(FontAwesomeIcon icon, string tooltip, Action onClick, Vector2? iconOffset = null) + { + _titleButtons.Add(new Window.TitleBarButton + { + Icon = icon, + IconOffset = iconOffset ?? new Vector2(2, 1), + Click = _ => onClick(), + ShowTooltip = () => UiSharedService.AttachToolTip(tooltip), + }); + return this; + } + + public Window Apply() + { + if (_titleButtons.Count > 0) + _window.TitleBarButtons = _titleButtons; + return _window; + } +} + +public static class WindowUtils +{ + public static Vector4 GetUidColor(this ServerState state) + { + return state switch + { + ServerState.Connecting => UIColors.Get("LightlessYellow"), + ServerState.Reconnecting => UIColors.Get("DimRed"), + ServerState.Connected => UIColors.Get("LightlessPurple"), + ServerState.Disconnected => UIColors.Get("LightlessYellow"), + ServerState.Disconnecting => UIColors.Get("LightlessYellow"), + ServerState.Unauthorized => UIColors.Get("DimRed"), + ServerState.VersionMisMatch => UIColors.Get("DimRed"), + ServerState.Offline => UIColors.Get("DimRed"), + ServerState.RateLimited => UIColors.Get("LightlessYellow"), + ServerState.NoSecretKey => UIColors.Get("LightlessYellow"), + ServerState.MultiChara => UIColors.Get("LightlessYellow"), + ServerState.OAuthMisconfigured => UIColors.Get("DimRed"), + ServerState.OAuthLoginTokenStale => UIColors.Get("DimRed"), + ServerState.NoAutoLogon => UIColors.Get("LightlessYellow"), + _ => UIColors.Get("DimRed"), + }; + } + + public static string GetUidText(this ServerState state, string displayName) + { + return state switch + { + ServerState.Reconnecting => "Reconnecting", + ServerState.Connecting => "Connecting", + ServerState.Disconnected => "Disconnected", + ServerState.Disconnecting => "Disconnecting", + ServerState.Unauthorized => "Unauthorized", + ServerState.VersionMisMatch => "Version mismatch", + ServerState.Offline => "Unavailable", + ServerState.RateLimited => "Rate Limited", + ServerState.NoSecretKey => "No Secret Key", + ServerState.MultiChara => "Duplicate Characters", + ServerState.OAuthMisconfigured => "Misconfigured OAuth2", + ServerState.OAuthLoginTokenStale => "Stale OAuth2", + ServerState.NoAutoLogon => "Auto Login disabled", + ServerState.Connected => displayName, + _ => string.Empty, + }; + } + + public static string GetServerError(this ServerState state, string authFailureMessage) + { + return state switch + { + ServerState.Connecting => "Attempting to connect to the server.", + ServerState.Reconnecting => "Connection to server interrupted, attempting to reconnect to the server.", + ServerState.Disconnected => "You are currently disconnected from the Lightless Sync server.", + ServerState.Disconnecting => "Disconnecting from the server", + ServerState.Unauthorized => "Server Response: " + authFailureMessage, + ServerState.Offline => "Your selected Lightless Sync server is currently offline.", + ServerState.VersionMisMatch => + "Your plugin or the server you are connecting to is out of date. Please update your plugin now. If you already did so, contact the server provider to update their server to the latest version.", + ServerState.RateLimited => "You are rate limited for (re)connecting too often. Disconnect, wait 10 minutes and try again.", + ServerState.NoSecretKey => "You have no secret key set for this current character. Open Settings -> Service Settings and set a secret key for the current character. You can reuse the same secret key for multiple characters.", + ServerState.MultiChara => "Your Character Configuration has multiple characters configured with same name and world. You will not be able to connect until you fix this issue. Remove the duplicates from the configuration in Settings -> Service Settings -> Character Management and reconnect manually after.", + ServerState.OAuthMisconfigured => "OAuth2 is enabled but not fully configured, verify in the Settings -> Service Settings that you have OAuth2 connected and, importantly, a UID assigned to your current character.", + ServerState.OAuthLoginTokenStale => "Your OAuth2 login token is stale and cannot be used to renew. Go to the Settings -> Service Settings and unlink then relink your OAuth2 configuration.", + ServerState.NoAutoLogon => "This character has automatic login into Lightless disabled. Press the connect button to connect to Lightless.", + _ => string.Empty, + }; + } +} diff --git a/OtterGui b/OtterGui index 18e62ab..1459e2b 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 18e62ab2d8b9ac7028a33707eb35f8f9c61f245a +Subproject commit 1459e2b8f5e1687f659836709e23571235d4206c diff --git a/Penumbra.Api b/Penumbra.Api index 704d62f..d520712 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 704d62f64f791b8cfd42363beaa464ad6f98ae48 +Subproject commit d52071290b48a1f2292023675b4b72365aef4cc0 diff --git a/Penumbra.String b/Penumbra.String index 4aac62e..462afac 160000 --- a/Penumbra.String +++ b/Penumbra.String @@ -1 +1 @@ -Subproject commit 4aac62e73b89a0c538a7a0a5c22822f15b13c0cc +Subproject commit 462afac558becebbe06b4e5be9b1b3c3f5a9b6d6