From 1c36db97dc23caff5ebede1d4b3714762cb27fad Mon Sep 17 00:00:00 2001 From: azyges Date: Wed, 3 Dec 2025 13:59:30 +0900 Subject: [PATCH] show focus target on visibility hover --- LightlessSync/Services/DalamudUtilService.cs | 93 ++++++++++++++++++-- LightlessSync/Services/Mediator/Messages.cs | 1 + LightlessSync/UI/CompactUI.cs | 60 +++++++++++++ LightlessSync/UI/Components/DrawUserPair.cs | 4 + 4 files changed, 149 insertions(+), 9 deletions(-) diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 3a2cb94..1bbef90 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -14,6 +14,7 @@ using LightlessSync.Interop; using LightlessSync.LightlessConfiguration; using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Handlers; +using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.ActorTracking; using LightlessSync.Services.Mediator; using LightlessSync.Utils; @@ -41,10 +42,13 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber private readonly ILogger _logger; private readonly IObjectTable _objectTable; private readonly ActorObjectService _actorObjectService; + private readonly ITargetManager _targetManager; private readonly PerformanceCollectorService _performanceCollector; private readonly LightlessConfigService _configService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly Lazy _pairFactory; + private PairUniqueIdentifier? _FocusPairIdent; + private IGameObject? _FocusOriginalTarget; private uint? _classJobId = 0; private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow; private string _lastGlobalBlockPlayer = string.Empty; @@ -68,6 +72,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber _gameData = gameData; _gameConfig = gameConfig; _actorObjectService = actorObjectService; + _targetManager = targetManager; _blockedCharacterHandler = blockedCharacterHandler; Mediator = mediator; _performanceCollector = performanceCollector; @@ -125,20 +130,24 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber mediator.Subscribe(this, (msg) => { if (clientState.IsPvP) return; - var pair = _pairFactory.Value.Create(msg.Pair.UniqueIdent) ?? msg.Pair; - var name = pair.PlayerName; - if (string.IsNullOrEmpty(name)) return; - if (!_actorObjectService.TryGetPlayerByName(name, out var descriptor)) - return; - var addr = descriptor.Address; - if (addr == nint.Zero) return; + if (!ResolvePairAddress(msg.Pair, out var pair, out var addr)) return; var useFocusTarget = _configService.Current.UseFocusTarget; _ = RunOnFrameworkThread(() => { + var gameObject = CreateGameObject(addr); + if (gameObject is null) return; if (useFocusTarget) - targetManager.FocusTarget = CreateGameObject(addr); + { + _targetManager.FocusTarget = gameObject; + if (_FocusPairIdent.HasValue && _FocusPairIdent.Value.Equals(pair.UniqueIdent)) + { + _FocusOriginalTarget = _targetManager.FocusTarget; + } + } else - targetManager.Target = CreateGameObject(addr); + { + _targetManager.Target = gameObject; + } }).ConfigureAwait(false); }); IsWine = Util.IsWine(); @@ -148,6 +157,61 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber private Lazy RebuildCID() => new(GetCID); public bool IsWine { get; init; } + private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address) + { + resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair; + address = nint.Zero; + var name = resolvedPair.PlayerName; + if (string.IsNullOrEmpty(name)) return false; + if (!_actorObjectService.TryGetPlayerByName(name, out var descriptor)) + return false; + address = descriptor.Address; + return address != nint.Zero; + } + + public void FocusVisiblePair(Pair pair) + { + if (_clientState.IsPvP) return; + if (!ResolvePairAddress(pair, out var resolvedPair, out var address)) return; + _ = RunOnFrameworkThread(() => FocusPairUnsafe(address, resolvedPair.UniqueIdent)); + } + + public void ReleaseVisiblePairFocus() + { + _ = RunOnFrameworkThread(ReleaseFocusUnsafe); + } + + private void FocusPairUnsafe(nint address, PairUniqueIdentifier pairIdent) + { + var target = CreateGameObject(address); + if (target is null) return; + + if (!_FocusPairIdent.HasValue) + { + _FocusOriginalTarget = _targetManager.FocusTarget; + } + + _targetManager.FocusTarget = target; + _FocusPairIdent = pairIdent; + } + + private void ReleaseFocusUnsafe() + { + if (!_FocusPairIdent.HasValue) + { + return; + } + + var previous = _FocusOriginalTarget; + if (previous != null && !IsObjectPresent(previous)) + { + previous = null; + } + + _targetManager.FocusTarget = previous; + _FocusPairIdent = null; + _FocusOriginalTarget = null; + } public unsafe GameObject* GposeTarget { @@ -505,6 +569,17 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber Mediator.UnsubscribeAll(this); _framework.Update -= FrameworkOnUpdate; + if (_FocusPairIdent.HasValue) + { + if (_framework.IsInFrameworkUpdateThread) + { + ReleaseFocusUnsafe(); + } + else + { + _ = RunOnFrameworkThread(ReleaseFocusUnsafe); + } + } return Task.CompletedTask; } diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 33dd062..f8250b9 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -103,6 +103,7 @@ public record PairDataChangedMessage : MessageBase; public record PairUiUpdatedMessage(PairUiSnapshot Snapshot) : MessageBase; public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase; public record TargetPairMessage(Pair Pair) : MessageBase; +public record PairFocusCharacterMessage(Pair Pair) : SameThreadMessage; public record CombatStartMessage : MessageBase; public record CombatEndMessage : MessageBase; public record PerformanceStartMessage : MessageBase; diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index d2715c9..a40dd1b 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -56,6 +56,7 @@ public class CompactUi : WindowMediatorSubscriberBase private readonly TagHandler _tagHandler; private readonly UiSharedService _uiSharedService; private readonly LightFinderService _broadcastService; + private readonly DalamudUtilService _dalamudUtilService; private List _drawFolders; private Pair? _lastAddedUser; @@ -68,6 +69,9 @@ public class CompactUi : WindowMediatorSubscriberBase private float _windowContentWidth; private readonly SeluneBrush _seluneBrush = new(); private const float _connectButtonHighlightThickness = 14f; + private Pair? _focusedPair; + private Pair? _pendingFocusPair; + private int _pendingFocusFrame = -1; public CompactUi( ILogger logger, @@ -109,7 +113,9 @@ public class CompactUi : WindowMediatorSubscriberBase _ipcManager = ipcManager; _broadcastService = broadcastService; _pairLedger = pairLedger; + _dalamudUtilService = dalamudUtilService; _tabMenu = new TopTabMenu(Mediator, _apiController, _uiSharedService, pairRequestService, dalamudUtilService, lightlessNotificationService); + Mediator.Subscribe(this, msg => RegisterFocusCharacter(msg.Pair)); AllowPinning = true; AllowClickthrough = false; @@ -178,6 +184,12 @@ public class CompactUi : WindowMediatorSubscriberBase _lightlessMediator = mediator; } + public override void OnClose() + { + ForceReleaseFocus(); + base.OnClose(); + } + protected override void DrawInternal() { var drawList = ImGui.GetWindowDrawList(); @@ -268,6 +280,8 @@ public class CompactUi : WindowMediatorSubscriberBase selune.Animate(ImGui.GetIO().DeltaTime); } + ProcessFocusTracker(); + var lastAddedPair = _pairUiService.GetLastAddedPair(); if (_configService.Current.OpenPopupOnAdd && lastAddedPair is not null) { @@ -1117,4 +1131,50 @@ public class CompactUi : WindowMediatorSubscriberBase _wasOpen = IsOpen; IsOpen = false; } + + private void RegisterFocusCharacter(Pair pair) + { + _pendingFocusPair = pair; + _pendingFocusFrame = ImGui.GetFrameCount(); + } + + private void ProcessFocusTracker() + { + var frame = ImGui.GetFrameCount(); + Pair? character = _pendingFocusFrame == frame ? _pendingFocusPair : null; + if (!ReferenceEquals(character, _focusedPair)) + { + if (character is null) + { + _dalamudUtilService.ReleaseVisiblePairFocus(); + } + else + { + _dalamudUtilService.FocusVisiblePair(character); + } + + _focusedPair = character; + } + + if (_pendingFocusFrame == frame) + { + _pendingFocusPair = null; + _pendingFocusFrame = -1; + } + } + + private void ForceReleaseFocus() + { + if (_focusedPair is null) + { + _pendingFocusPair = null; + _pendingFocusFrame = -1; + return; + } + + _dalamudUtilService.ReleaseVisiblePairFocus(); + _focusedPair = null; + _pendingFocusPair = null; + _pendingFocusFrame = -1; + } } diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index 0c8b649..877fe6d 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -247,6 +247,10 @@ public class DrawUserPair else if (_pair.IsVisible) { _uiSharedService.IconText(FontAwesomeIcon.Eye, UIColors.Get("LightlessBlue")); + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem | ImGuiHoveredFlags.AllowWhenOverlapped | ImGuiHoveredFlags.AllowWhenDisabled)) + { + _mediator.Publish(new PairFocusCharacterMessage(_pair)); + } if (ImGui.IsItemClicked()) { _mediator.Publish(new TargetPairMessage(_pair));