From 924a3803d93a5efeec2b73f14dae865272b8a6ee Mon Sep 17 00:00:00 2001 From: Loporrit <141286461+loporrit@users.noreply.github.com> Date: Fri, 25 Jul 2025 18:11:16 +0000 Subject: [PATCH] Revert gpose actors on plugin unload or when NoSnap triggers --- .../Interop/Ipc/IpcCallerGlamourer.cs | 7 ++ .../Interop/Ipc/IpcCallerPenumbra.cs | 7 ++ .../Factories/PairHandlerFactory.cs | 6 +- .../PlayerData/Handlers/PairHandler.cs | 24 +++++- .../CharaData/CharaDataCharacterHandler.cs | 30 ++++++- MareSynchronos/Services/NoSnapService.cs | 85 ++++++++++++++++++- .../Services/PluginWatcherService.cs | 13 +++ 7 files changed, 165 insertions(+), 7 deletions(-) diff --git a/MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs b/MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs index 73b086e..aa083b0 100644 --- a/MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs +++ b/MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs @@ -204,6 +204,13 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC } } + public void RevertNow(ILogger logger, Guid applicationId, int objectIndex) + { + if ((!APIAvailable) || _dalamudUtil.IsZoning) return; + logger.LogTrace("[{applicationId}] Immediately reverting object index {objId}", applicationId, objectIndex); + _glamourerRevert.Invoke(objectIndex, LockCode); + } + public async Task RevertByNameAsync(ILogger logger, string name, Guid applicationId) { if ((!APIAvailable) || _dalamudUtil.IsZoning) return; diff --git a/MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs b/MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs index eae4c9a..992989a 100644 --- a/MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs +++ b/MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs @@ -271,6 +271,13 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa } } + public void RedrawNow(ILogger logger, Guid applicationId, int objectIndex) + { + if (!APIAvailable || _dalamudUtil.IsZoning) return; + logger.LogTrace("[{applicationId}] Immediately redrawing object index {objId}", applicationId, objectIndex); + _penumbraRedraw.Invoke(objectIndex); + } + public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collId) { if (!APIAvailable) return; diff --git a/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs index 0fadae8..145889a 100644 --- a/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PairHandlerFactory.cs @@ -27,13 +27,14 @@ public class PairHandlerFactory private readonly PluginWarningNotificationService _pluginWarningNotificationManager; private readonly PairAnalyzerFactory _pairAnalyzerFactory; private readonly VisibilityService _visibilityService; + private readonly NoSnapService _noSnapService; public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager, FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService, PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime, FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService, ServerConfigurationManager serverConfigManager, PairAnalyzerFactory pairAnalyzerFactory, - MareConfigService configService, VisibilityService visibilityService) + MareConfigService configService, VisibilityService visibilityService, NoSnapService noSnapService) { _loggerFactory = loggerFactory; _gameObjectHandlerFactory = gameObjectHandlerFactory; @@ -49,12 +50,13 @@ public class PairHandlerFactory _pairAnalyzerFactory = pairAnalyzerFactory; _configService = configService; _visibilityService = visibilityService; + _noSnapService = noSnapService; } public PairHandler Create(Pair pair) { return new PairHandler(_loggerFactory.CreateLogger(), pair, _pairAnalyzerFactory.Create(pair), _gameObjectHandlerFactory, _ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime, - _fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager, _configService, _visibilityService); + _fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager, _configService, _visibilityService, _noSnapService); } } \ No newline at end of file diff --git a/MareSynchronos/PlayerData/Handlers/PairHandler.cs b/MareSynchronos/PlayerData/Handlers/PairHandler.cs index fa52bf3..5e57927 100644 --- a/MareSynchronos/PlayerData/Handlers/PairHandler.cs +++ b/MareSynchronos/PlayerData/Handlers/PairHandler.cs @@ -33,6 +33,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase private readonly ServerConfigurationManager _serverConfigManager; private readonly PluginWarningNotificationService _pluginWarningNotificationManager; private readonly VisibilityService _visibilityService; + private readonly NoSnapService _noSnapService; private CancellationTokenSource? _applicationCancellationTokenSource = new(); private Guid _applicationId; private Task? _applicationTask; @@ -55,7 +56,8 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase FileCacheManager fileDbManager, MareMediator mediator, PlayerPerformanceService playerPerformanceService, ServerConfigurationManager serverConfigManager, - MareConfigService configService, VisibilityService visibilityService) : base(logger, mediator) + MareConfigService configService, VisibilityService visibilityService, + NoSnapService noSnapService) : base(logger, mediator) { Pair = pair; PairAnalyzer = pairAnalyzer; @@ -69,6 +71,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase _serverConfigManager = serverConfigManager; _configService = configService; _visibilityService = visibilityService; + _noSnapService = noSnapService; _visibilityService.StartTracking(Pair.Ident); @@ -317,6 +320,24 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase }); } + private void RegisterGposeClones() + { + var name = PlayerName; + if (name == null) + return; + _ = _dalamudUtil.RunOnFrameworkThread(() => + { + foreach (var actor in _dalamudUtil.GetGposeCharactersFromObjectTable()) + { + if (actor == null) continue; + var gposeName = actor.Name.TextValue; + if (!name.Equals(gposeName, StringComparison.Ordinal)) + continue; + _noSnapService.AddGposer(actor.ObjectIndex); + } + }); + } + private async Task UndoApplicationAsync(Guid applicationId = default) { Logger.LogDebug($"Undoing application of {Pair.UserPair}"); @@ -333,6 +354,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase { await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).ConfigureAwait(false); _penumbraCollection = Guid.Empty; + RegisterGposeClones(); } if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name)) diff --git a/MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs b/MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs index 85162f7..21e390f 100644 --- a/MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs +++ b/MareSynchronos/Services/CharaData/CharaDataCharacterHandler.cs @@ -13,18 +13,20 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; private readonly DalamudUtilService _dalamudUtilService; private readonly IpcManager _ipcManager; + private readonly NoSnapService _noSnapService; private readonly Dictionary _handledCharaData = new(StringComparer.Ordinal); public IReadOnlyDictionary HandledCharaData => _handledCharaData; public CharaDataCharacterHandler(ILogger logger, MareMediator mediator, GameObjectHandlerFactory gameObjectHandlerFactory, DalamudUtilService dalamudUtilService, - IpcManager ipcManager) + IpcManager ipcManager, NoSnapService noSnapService) : base(logger, mediator) { _gameObjectHandlerFactory = gameObjectHandlerFactory; _dalamudUtilService = dalamudUtilService; _ipcManager = ipcManager; + _noSnapService = noSnapService; mediator.Subscribe(this, msg => { foreach (var chara in _handledCharaData) @@ -90,13 +92,18 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase { if (handled == null) return false; _handledCharaData.Remove(handled.Name); - await _dalamudUtilService.RunOnFrameworkThread(() => RevertChara(handled.Name, handled.CustomizePlus)).ConfigureAwait(false); + await _dalamudUtilService.RunOnFrameworkThread(async () => + { + RemoveGposer(handled); + await RevertChara(handled.Name, handled.CustomizePlus).ConfigureAwait(false); + }).ConfigureAwait(false); return true; } internal void AddHandledChara(HandledCharaDataEntry handledCharaDataEntry) { _handledCharaData.Add(handledCharaDataEntry.Name, handledCharaDataEntry); + _ = _dalamudUtilService.RunOnFrameworkThread(() => AddGposer(handledCharaDataEntry)); } public void UpdateHandledData(Dictionary newData) @@ -127,4 +134,23 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase if (handler.Address == nint.Zero) return null; return handler; } + + private int GetGposerObjectIndex(string name) + { + return _dalamudUtilService.GetGposeCharacterFromObjectTableByName(name, _dalamudUtilService.IsInGpose)?.ObjectIndex ?? -1; + } + + private void AddGposer(HandledCharaDataEntry handled) + { + int objectIndex = GetGposerObjectIndex(handled.Name); + if (objectIndex > 0) + _noSnapService.AddGposer(objectIndex); + } + + private void RemoveGposer(HandledCharaDataEntry handled) + { + int objectIndex = GetGposerObjectIndex(handled.Name); + if (objectIndex > 0) + _noSnapService.RemoveGposer(objectIndex); + } } diff --git a/MareSynchronos/Services/NoSnapService.cs b/MareSynchronos/Services/NoSnapService.cs index ebd84b3..9214b5e 100644 --- a/MareSynchronos/Services/NoSnapService.cs +++ b/MareSynchronos/Services/NoSnapService.cs @@ -1,4 +1,5 @@ using Dalamud.Plugin; +using MareSynchronos.Interop.Ipc; using MareSynchronos.MareConfiguration.Models; using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Hosting; @@ -15,16 +16,23 @@ public class NoSnapService : IHostedService, IMediatorSubscriber ["Snappy"] = false, ["Meddle.Plugin"] = false }; + private static readonly HashSet _gposers = new(); + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly DalamudUtilService _dalamudUtilService; + private readonly IpcManager _ipcManager; public static bool AnyLoaded { get; private set; } = false; public MareMediator Mediator { get; init; } - public NoSnapService(ILogger logger, IDalamudPluginInterface pi, MareMediator mediator) + public NoSnapService(ILogger logger, IDalamudPluginInterface pi, MareMediator mediator, + IHostApplicationLifetime hostApplicationLifetime, DalamudUtilService dalamudUtilService, IpcManager ipcManager) { _logger = logger; Mediator = mediator; - + _hostApplicationLifetime = hostApplicationLifetime; + _dalamudUtilService = dalamudUtilService; + _ipcManager = ipcManager; foreach (var pluginName in _listOfPlugins.Keys) { var plugin = pi.InstalledPlugins.FirstOrDefault(p => p.InternalName.Equals(pluginName, StringComparison.Ordinal)); @@ -38,9 +46,80 @@ public class NoSnapService : IHostedService, IMediatorSubscriber }); } + Mediator.Subscribe(this, msg => ClearGposeList()); + Mediator.Subscribe(this, msg => ClearGposeList()); + Update(); } + public void AddGposer(int objectIndex) + { + if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested) + { + _logger.LogInformation("Immediately reverting object index {id}", objectIndex); + try + { + Guid applicationId = Guid.NewGuid(); + _ipcManager.Glamourer.RevertNow(_logger, applicationId, objectIndex); + _ipcManager.Penumbra.RedrawNow(_logger, applicationId, objectIndex); + } + catch { } + return; + } + + _logger.LogInformation("Registering gposer object index {id}", objectIndex); + lock (_gposers) + _gposers.Add(objectIndex); + } + + public void RemoveGposer(int objectIndex) + { + _logger.LogInformation("Un-registering gposer object index {id}", objectIndex); + lock (_gposers) + _gposers.Remove(objectIndex); + } + + private void ClearGposeList() + { + if (_gposers.Count > 0) + _logger.LogInformation("Clearing gposer list"); + lock (_gposers) + _gposers.Clear(); + } + + private void RevertGposers() + { + List? gposersList = null; + + lock (_gposers) + { + if (_gposers.Count > 0) + { + _logger.LogInformation("Reverting gposers"); + gposersList = _gposers.ToList(); + _gposers.Clear(); + } + } + + if (gposersList == null) + return; + + _dalamudUtilService.RunOnFrameworkThread(() => + { + Guid applicationId = Guid.NewGuid(); + + foreach (var gposer in gposersList) + { + try + { + _ipcManager.Glamourer.RevertNow(_logger, applicationId, gposer); + _ipcManager.Penumbra.RedrawNow(_logger, applicationId, gposer); + } + catch { } + } + }).GetAwaiter().GetResult(); + } + public Task StartAsync(CancellationToken cancellationToken) { return Task.CompletedTask; @@ -48,6 +127,7 @@ public class NoSnapService : IHostedService, IMediatorSubscriber public Task StopAsync(CancellationToken cancellationToken) { + RevertGposers(); return Task.CompletedTask; } @@ -63,6 +143,7 @@ public class NoSnapService : IHostedService, IMediatorSubscriber if (AnyLoaded) { + RevertGposers(); var pluginList = string.Join(", ", _listOfPlugins.Where(p => p.Value).Select(p => p.Key)); Mediator.Publish(new NotificationMessage("Incompatible plugin loaded", $"Synced player appearances will not apply until incompatible plugins are disabled: {pluginList}.", NotificationType.Error)); diff --git a/MareSynchronos/Services/PluginWatcherService.cs b/MareSynchronos/Services/PluginWatcherService.cs index 98af785..a3528c5 100644 --- a/MareSynchronos/Services/PluginWatcherService.cs +++ b/MareSynchronos/Services/PluginWatcherService.cs @@ -75,6 +75,19 @@ public class PluginWatcherService : MediatorSubscriberBase Logger.LogError(e, "PluginWatcherService exception"); } }); + + // Continue scanning plugins during gpose as well + Mediator.Subscribe(this, (_) => + { + try + { + Update(); + } + catch (Exception e) + { + Logger.LogError(e, "PluginWatcherService exception"); + } + }); } private void Update()