From 6bf60fe729ff6c405b33581c3fcd83a9f3eb80fd Mon Sep 17 00:00:00 2001 From: Loporrit <141286461+loporrit@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:05:31 +0000 Subject: [PATCH] Implement IpcCallerMare and hand Mare control of any handled pairs --- MareSynchronos/Interop/Ipc/IpcCallerMare.cs | 44 +++++++++++ .../PlayerData/Handlers/PairHandler.cs | 20 ++++- MareSynchronos/Plugin.cs | 1 + MareSynchronos/Services/Mediator/Messages.cs | 2 +- MareSynchronos/Services/VisibilityService.cs | 75 ++++++++++++++++--- 5 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 MareSynchronos/Interop/Ipc/IpcCallerMare.cs diff --git a/MareSynchronos/Interop/Ipc/IpcCallerMare.cs b/MareSynchronos/Interop/Ipc/IpcCallerMare.cs new file mode 100644 index 0000000..ec32833 --- /dev/null +++ b/MareSynchronos/Interop/Ipc/IpcCallerMare.cs @@ -0,0 +1,44 @@ +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; +using MareSynchronos.Services; +using MareSynchronos.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace MareSynchronos.Interop.Ipc; + +public sealed class IpcCallerMare : DisposableMediatorSubscriberBase +{ + private readonly ICallGateSubscriber> _mareHandledGameAddresses; + private readonly List _emptyList = []; + + private bool _pluginLoaded; + + public IpcCallerMare(ILogger logger, IDalamudPluginInterface pi, MareMediator mediator) : base(logger, mediator) + { + _mareHandledGameAddresses = pi.GetIpcSubscriber>("MareSynchronos.GetHandledAddresses"); + + _pluginLoaded = PluginWatcherService.GetInitialPluginState(pi, "MareSynchronos")?.IsLoaded ?? false; + + Mediator.SubscribeKeyed(this, "MareSynchronos", (msg) => + { + _pluginLoaded = msg.IsLoaded; + }); + } + + public bool APIAvailable { get; private set; } = false; + + // Must be called on framework thread + public IReadOnlyList GetHandledGameAddresses() + { + if (!_pluginLoaded) return _emptyList; + + try + { + return _mareHandledGameAddresses.InvokeFunc(); + } + catch + { + return _emptyList; + } + } +} diff --git a/MareSynchronos/PlayerData/Handlers/PairHandler.cs b/MareSynchronos/PlayerData/Handlers/PairHandler.cs index dc9c44c..d12a347 100644 --- a/MareSynchronos/PlayerData/Handlers/PairHandler.cs +++ b/MareSynchronos/PlayerData/Handlers/PairHandler.cs @@ -74,7 +74,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase _visibilityService.StartTracking(Pair.Ident); - Mediator.SubscribeKeyed(this, Pair.Ident, (msg) => UpdateVisibility(msg.IsVisible)); + Mediator.SubscribeKeyed(this, Pair.Ident, (msg) => UpdateVisibility(msg.IsVisible, msg.Invalidate)); Mediator.Subscribe(this, (_) => { @@ -694,7 +694,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase } } - private void UpdateVisibility(bool nowVisible) + private void UpdateVisibility(bool nowVisible, bool invalidate = false) { if (string.IsNullOrEmpty(PlayerName)) { @@ -707,6 +707,22 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase $"Initializing User For Character {pc.Name}"))); } + // This was triggered by the character becoming handled by Mare, so unapply everything + // There seems to be a good chance that this races Mare and then crashes + if (!nowVisible && invalidate) + { + bool wasVisible = IsVisible; + IsVisible = false; + _charaHandler?.Invalidate(); + _downloadCancellationTokenSource?.CancelDispose(); + _downloadCancellationTokenSource = null; + if (wasVisible) + Logger.LogTrace("{this} visibility changed, now: {visi}", this, IsVisible); + Logger.LogDebug("Invalidating {this}", this); + UndoApplication(); + return; + } + if (!IsVisible && nowVisible) { // This is deferred application attempt, avoid any log output diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 794eb05..9944566 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -140,6 +140,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); diff --git a/MareSynchronos/Services/Mediator/Messages.cs b/MareSynchronos/Services/Mediator/Messages.cs index d314de3..662b2e7 100644 --- a/MareSynchronos/Services/Mediator/Messages.cs +++ b/MareSynchronos/Services/Mediator/Messages.cs @@ -73,7 +73,7 @@ public record ProfilePopoutToggle(Pair? Pair) : MessageBase; public record CompactUiChange(Vector2 Size, Vector2 Position) : MessageBase; public record ProfileOpenStandaloneMessage(Pair Pair) : MessageBase; public record RemoveWindowMessage(WindowMediatorSubscriberBase Window) : MessageBase; -public record PlayerVisibilityMessage(string Ident, bool IsVisible) : KeyedMessage(Ident, SameThread: true); +public record PlayerVisibilityMessage(string Ident, bool IsVisible, bool Invalidate = false) : KeyedMessage(Ident, SameThread: true); public record PairHandlerVisibleMessage(PairHandler Player) : MessageBase; public record OpenReportPopupMessage(Pair PairToReport) : MessageBase; public record OpenBanUserPopupMessage(Pair PairToBan, GroupFullInfoDto GroupFullInfoDto) : MessageBase; diff --git a/MareSynchronos/Services/VisibilityService.cs b/MareSynchronos/Services/VisibilityService.cs index 232c615..2731c17 100644 --- a/MareSynchronos/Services/VisibilityService.cs +++ b/MareSynchronos/Services/VisibilityService.cs @@ -1,3 +1,4 @@ +using MareSynchronos.Interop.Ipc; using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; @@ -7,19 +8,32 @@ namespace MareSynchronos.Services; // Detect when players of interest are visible public class VisibilityService : DisposableMediatorSubscriberBase { - private readonly DalamudUtilService _dalamudUtil; - private readonly ConcurrentDictionary _trackedPlayerVisibility = new(StringComparer.Ordinal); + private enum TrackedPlayerStatus + { + NotVisible, + Visible, + MareHandled + }; - public VisibilityService(ILogger logger, MareMediator mediator, DalamudUtilService dalamudUtil) + private readonly DalamudUtilService _dalamudUtil; + private readonly ConcurrentDictionary _trackedPlayerVisibility = new(StringComparer.Ordinal); + private readonly List _makeVisibleNextFrame = new(); + private readonly IpcCallerMare _mare; + private readonly HashSet cachedMareAddresses = new(); + private uint _cachedAddressSum = 0; + private uint _cachedAddressSumDebounce = 1; + + public VisibilityService(ILogger logger, MareMediator mediator, IpcCallerMare mare, DalamudUtilService dalamudUtil) : base(logger, mediator) { + _mare = mare; _dalamudUtil = dalamudUtil; Mediator.Subscribe(this, (_) => FrameworkUpdate()); } public void StartTracking(string ident) { - _trackedPlayerVisibility.TryAdd(ident, value: false); + _trackedPlayerVisibility.TryAdd(ident, TrackedPlayerStatus.NotVisible); } public void StopTracking(string ident) @@ -30,21 +44,62 @@ public class VisibilityService : DisposableMediatorSubscriberBase private void FrameworkUpdate() { + var mareHandledAddresses = _mare.GetHandledGameAddresses(); + uint addressSum = 0; + + foreach (var addr in mareHandledAddresses) + addressSum ^= (uint)addr.GetHashCode(); + + if (addressSum != _cachedAddressSum) + { + if (addressSum == _cachedAddressSumDebounce) + { + cachedMareAddresses.Clear(); + foreach (var addr in mareHandledAddresses) + cachedMareAddresses.Add(addr); + _cachedAddressSum = addressSum; + } + else + { + _cachedAddressSumDebounce = addressSum; + } + } + foreach (var player in _trackedPlayerVisibility) { string ident = player.Key; var findResult = _dalamudUtil.FindPlayerByNameHash(ident); + var isMareHandled = cachedMareAddresses.Contains(findResult.Address); + var isVisible = findResult.ObjectId != 0 && !isMareHandled; - if (!player.Value && findResult.ObjectId != 0) + if (player.Value == TrackedPlayerStatus.MareHandled && !isMareHandled) + _trackedPlayerVisibility.TryUpdate(ident, newValue: TrackedPlayerStatus.NotVisible, comparisonValue: TrackedPlayerStatus.MareHandled); + + if (player.Value == TrackedPlayerStatus.NotVisible && isVisible) { - if (_trackedPlayerVisibility.TryUpdate(ident, newValue: true, comparisonValue: false)) - Mediator.Publish(new(ident, IsVisible: true)); + if (_makeVisibleNextFrame.Contains(ident)) + { + if (_trackedPlayerVisibility.TryUpdate(ident, newValue: TrackedPlayerStatus.Visible, comparisonValue: TrackedPlayerStatus.NotVisible)) + Mediator.Publish(new(ident, IsVisible: true)); + } + else + _makeVisibleNextFrame.Add(ident); } - else if (player.Value && findResult.ObjectId == 0) + else if (player.Value == TrackedPlayerStatus.NotVisible && isMareHandled) { - if (_trackedPlayerVisibility.TryUpdate(ident, newValue: false, comparisonValue: true)) - Mediator.Publish(new(ident, IsVisible: false)); + // Send a technically redundant visibility update with the added intent of triggering PairHandler to undo the application by name + if (_trackedPlayerVisibility.TryUpdate(ident, newValue: TrackedPlayerStatus.MareHandled, comparisonValue: TrackedPlayerStatus.NotVisible)) + Mediator.Publish(new(ident, IsVisible: false, Invalidate: true)); } + else if (player.Value == TrackedPlayerStatus.Visible && !isVisible) + { + var newTrackedStatus = isMareHandled ? TrackedPlayerStatus.MareHandled : TrackedPlayerStatus.NotVisible; + if (_trackedPlayerVisibility.TryUpdate(ident, newValue: newTrackedStatus, comparisonValue: TrackedPlayerStatus.Visible)) + Mediator.Publish(new(ident, IsVisible: false, Invalidate: isMareHandled)); + } + + if (!isVisible) + _makeVisibleNextFrame.Remove(ident); } } } \ No newline at end of file