From d995afcf484b4b5d5df9289553011864f0612b9b Mon Sep 17 00:00:00 2001 From: azyges Date: Fri, 28 Nov 2025 00:33:46 +0900 Subject: [PATCH] work done on the ipc --- .../Interop/Ipc/Framework/IpcFramework.cs | 193 ++++++ LightlessSync/Interop/Ipc/IIpcCaller.cs | 7 - LightlessSync/Interop/Ipc/IpcCallerBrio.cs | 48 +- .../Interop/Ipc/IpcCallerCustomize.cs | 34 +- .../Interop/Ipc/IpcCallerGlamourer.cs | 90 +-- LightlessSync/Interop/Ipc/IpcCallerHeels.cs | 40 +- .../Interop/Ipc/IpcCallerHonorific.cs | 45 +- LightlessSync/Interop/Ipc/IpcCallerMoodles.cs | 44 +- .../Interop/Ipc/IpcCallerPenumbra.cs | 630 +++++------------- .../Interop/Ipc/IpcCallerPetNames.cs | 62 +- LightlessSync/Interop/Ipc/IpcProvider.cs | 60 +- .../Interop/Ipc/Penumbra/PenumbraBase.cs | 27 + .../Ipc/Penumbra/PenumbraCollections.cs | 197 ++++++ .../Interop/Ipc/Penumbra/PenumbraRedraw.cs | 81 +++ .../Interop/Ipc/Penumbra/PenumbraResource.cs | 141 ++++ .../Interop/Ipc/Penumbra/PenumbraTexture.cs | 121 ++++ LightlessSync/Plugin.cs | 2 +- 17 files changed, 1199 insertions(+), 623 deletions(-) create mode 100644 LightlessSync/Interop/Ipc/Framework/IpcFramework.cs delete mode 100644 LightlessSync/Interop/Ipc/IIpcCaller.cs create mode 100644 LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs create mode 100644 LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs create mode 100644 LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs create mode 100644 LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs create mode 100644 LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs diff --git a/LightlessSync/Interop/Ipc/Framework/IpcFramework.cs b/LightlessSync/Interop/Ipc/Framework/IpcFramework.cs new file mode 100644 index 0000000..dbf1c15 --- /dev/null +++ b/LightlessSync/Interop/Ipc/Framework/IpcFramework.cs @@ -0,0 +1,193 @@ +using Dalamud.Plugin; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.Interop.Ipc.Framework; + +public enum IpcConnectionState +{ + Unknown = 0, + MissingPlugin = 1, + VersionMismatch = 2, + PluginDisabled = 3, + NotReady = 4, + Available = 5, + Error = 6, +} + +public sealed record IpcServiceDescriptor(string InternalName, string DisplayName, Version MinimumVersion) +{ + public override string ToString() + => $"{DisplayName} (>= {MinimumVersion})"; +} + +public interface IIpcService : IDisposable +{ + IpcServiceDescriptor Descriptor { get; } + IpcConnectionState State { get; } + IDalamudPluginInterface PluginInterface { get; } + bool APIAvailable { get; } + void CheckAPI(); +} + +public interface IIpcInterop : IDisposable +{ + string Name { get; } + void OnConnectionStateChanged(IpcConnectionState state); +} + +public abstract class IpcInteropBase : IIpcInterop +{ + protected IpcInteropBase(ILogger logger) + { + Logger = logger; + } + + protected ILogger Logger { get; } + + protected IpcConnectionState State { get; private set; } = IpcConnectionState.Unknown; + + protected bool IsAvailable => State == IpcConnectionState.Available; + + public abstract string Name { get; } + + public void OnConnectionStateChanged(IpcConnectionState state) + { + if (State == state) + { + return; + } + + var previous = State; + State = state; + HandleStateChange(previous, state); + } + + protected abstract void HandleStateChange(IpcConnectionState previous, IpcConnectionState current); + + public virtual void Dispose() + { + } +} + +public abstract class IpcServiceBase : DisposableMediatorSubscriberBase, IIpcService +{ + private readonly List _interops = new(); + + protected IpcServiceBase( + ILogger logger, + LightlessMediator mediator, + IDalamudPluginInterface pluginInterface, + IpcServiceDescriptor descriptor) : base(logger, mediator) + { + PluginInterface = pluginInterface; + Descriptor = descriptor; + } + + protected IDalamudPluginInterface PluginInterface { get; } + + IDalamudPluginInterface IIpcService.PluginInterface => PluginInterface; + + protected IpcServiceDescriptor Descriptor { get; } + + IpcServiceDescriptor IIpcService.Descriptor => Descriptor; + + public IpcConnectionState State { get; private set; } = IpcConnectionState.Unknown; + + public bool APIAvailable => State == IpcConnectionState.Available; + + public virtual void CheckAPI() + { + var newState = EvaluateState(); + UpdateState(newState); + } + + protected virtual IpcConnectionState EvaluateState() + { + try + { + var plugin = PluginInterface.InstalledPlugins + .FirstOrDefault(p => string.Equals(p.InternalName, Descriptor.InternalName, StringComparison.OrdinalIgnoreCase)); + + if (plugin == null) + { + return IpcConnectionState.MissingPlugin; + } + + if (plugin.Version < Descriptor.MinimumVersion) + { + return IpcConnectionState.VersionMismatch; + } + + if (!IsPluginEnabled()) + { + return IpcConnectionState.PluginDisabled; + } + + if (!IsPluginReady()) + { + return IpcConnectionState.NotReady; + } + + return IpcConnectionState.Available; + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Failed to evaluate IPC state for {Service}", Descriptor.DisplayName); + return IpcConnectionState.Error; + } + } + + protected virtual bool IsPluginEnabled() + => true; + + protected virtual bool IsPluginReady() + => true; + + protected TInterop RegisterInterop(TInterop interop) + where TInterop : IIpcInterop + { + _interops.Add(interop); + interop.OnConnectionStateChanged(State); + return interop; + } + + private void UpdateState(IpcConnectionState newState) + { + if (State == newState) + { + return; + } + + var previous = State; + State = newState; + OnConnectionStateChanged(previous, newState); + + foreach (var interop in _interops) + { + interop.OnConnectionStateChanged(newState); + } + } + + protected virtual void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current) + { + Logger.LogTrace("{Service} IPC state transitioned from {Previous} to {Current}", Descriptor.DisplayName, previous, current); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (!disposing) + { + return; + } + + for (var i = _interops.Count - 1; i >= 0; --i) + { + _interops[i].Dispose(); + } + + _interops.Clear(); + } +} diff --git a/LightlessSync/Interop/Ipc/IIpcCaller.cs b/LightlessSync/Interop/Ipc/IIpcCaller.cs deleted file mode 100644 index 8519d1a..0000000 --- a/LightlessSync/Interop/Ipc/IIpcCaller.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace LightlessSync.Interop.Ipc; - -public interface IIpcCaller : IDisposable -{ - bool APIAvailable { get; } - void CheckAPI(); -} diff --git a/LightlessSync/Interop/Ipc/IpcCallerBrio.cs b/LightlessSync/Interop/Ipc/IpcCallerBrio.cs index 5728464..83105d9 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerBrio.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerBrio.cs @@ -2,15 +2,19 @@ using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using LightlessSync.API.Dto.CharaData; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; +using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; using System.Numerics; using System.Text.Json.Nodes; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerBrio : IIpcCaller +public sealed class IpcCallerBrio : IpcServiceBase { + private static readonly IpcServiceDescriptor BrioDescriptor = new("Brio", "Brio", new Version(0, 0, 0, 0)); + private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtilService; private readonly ICallGateSubscriber<(int, int)> _brioApiVersion; @@ -25,10 +29,8 @@ public sealed class IpcCallerBrio : IIpcCaller private readonly ICallGateSubscriber _brioFreezePhysics; - public bool APIAvailable { get; private set; } - public IpcCallerBrio(ILogger logger, IDalamudPluginInterface dalamudPluginInterface, - DalamudUtilService dalamudUtilService) + DalamudUtilService dalamudUtilService, LightlessMediator mediator) : base(logger, mediator, dalamudPluginInterface, BrioDescriptor) { _logger = logger; _dalamudUtilService = dalamudUtilService; @@ -46,19 +48,6 @@ public sealed class IpcCallerBrio : IIpcCaller CheckAPI(); } - public void CheckAPI() - { - try - { - var version = _brioApiVersion.InvokeFunc(); - APIAvailable = (version.Item1 == 2 && version.Item2 >= 0); - } - catch - { - APIAvailable = false; - } - } - public async Task SpawnActorAsync() { if (!APIAvailable) return null; @@ -140,7 +129,30 @@ public sealed class IpcCallerBrio : IIpcCaller return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetPoseFromJson.InvokeFunc(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false); } - public void Dispose() + protected override IpcConnectionState EvaluateState() { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + var version = _brioApiVersion.InvokeFunc(); + return version.Item1 == 2 && version.Item2 >= 0 + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Brio IPC version"); + return IpcConnectionState.Error; + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerCustomize.cs b/LightlessSync/Interop/Ipc/IpcCallerCustomize.cs index 60feaba..fd1b88c 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerCustomize.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerCustomize.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using Dalamud.Utility; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; @@ -9,8 +10,10 @@ using System.Text; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerCustomize : IIpcCaller +public sealed class IpcCallerCustomize : IpcServiceBase { + private static readonly IpcServiceDescriptor CustomizeDescriptor = new("CustomizePlus", "Customize+", new Version(0, 0, 0, 0)); + private readonly ICallGateSubscriber<(int, int)> _customizePlusApiVersion; private readonly ICallGateSubscriber _customizePlusGetActiveProfile; private readonly ICallGateSubscriber _customizePlusGetProfileById; @@ -23,7 +26,7 @@ public sealed class IpcCallerCustomize : IIpcCaller private readonly LightlessMediator _lightlessMediator; public IpcCallerCustomize(ILogger logger, IDalamudPluginInterface dalamudPluginInterface, - DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator) + DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator) : base(logger, lightlessMediator, dalamudPluginInterface, CustomizeDescriptor) { _customizePlusApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("CustomizePlus.General.GetApiVersion"); _customizePlusGetActiveProfile = dalamudPluginInterface.GetIpcSubscriber("CustomizePlus.Profile.GetActiveProfileIdOnCharacter"); @@ -41,8 +44,6 @@ public sealed class IpcCallerCustomize : IIpcCaller CheckAPI(); } - public bool APIAvailable { get; private set; } = false; - public async Task RevertAsync(nint character) { if (!APIAvailable) return; @@ -113,16 +114,25 @@ public sealed class IpcCallerCustomize : IIpcCaller return Convert.ToBase64String(Encoding.UTF8.GetBytes(scale)); } - public void CheckAPI() + protected override IpcConnectionState EvaluateState() { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + try { var version = _customizePlusApiVersion.InvokeFunc(); - APIAvailable = (version.Item1 == 6 && version.Item2 >= 0); + return version.Item1 == 6 && version.Item2 >= 0 + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; } - catch + catch (Exception ex) { - APIAvailable = false; + Logger.LogDebug(ex, "Failed to query Customize+ API version"); + return IpcConnectionState.Error; } } @@ -132,8 +142,14 @@ public sealed class IpcCallerCustomize : IIpcCaller _lightlessMediator.Publish(new CustomizePlusMessage(obj?.Address ?? null)); } - public void Dispose() + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + if (!disposing) + { + return; + } + _customizePlusOnScaleUpdate.Unsubscribe(OnCustomizePlusScaleChange); } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerGlamourer.cs b/LightlessSync/Interop/Ipc/IpcCallerGlamourer.cs index 8763188..4f9f53d 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerGlamourer.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerGlamourer.cs @@ -2,6 +2,7 @@ using Dalamud.Plugin; using Glamourer.Api.Helpers; using Glamourer.Api.IpcSubscribers; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Handlers; using LightlessSync.Services; @@ -10,8 +11,9 @@ using Microsoft.Extensions.Logging; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcCaller +public sealed class IpcCallerGlamourer : IpcServiceBase { + private static readonly IpcServiceDescriptor GlamourerDescriptor = new("Glamourer", "Glamourer", new Version(1, 3, 0, 10)); private readonly ILogger _logger; private readonly IDalamudPluginInterface _pi; private readonly DalamudUtilService _dalamudUtil; @@ -31,7 +33,7 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC private readonly uint LockCode = 0x6D617265; public IpcCallerGlamourer(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator, - RedrawManager redrawManager) : base(logger, lightlessMediator) + RedrawManager redrawManager) : base(logger, lightlessMediator, pi, GlamourerDescriptor) { _glamourerApiVersions = new ApiVersion(pi); _glamourerGetAllCustomization = new GetStateBase64(pi); @@ -62,47 +64,6 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC _glamourerStateChanged?.Dispose(); } - public bool APIAvailable { get; private set; } - - public void CheckAPI() - { - bool apiAvailable = false; - try - { - bool versionValid = (_pi.InstalledPlugins - .FirstOrDefault(p => string.Equals(p.InternalName, "Glamourer", StringComparison.OrdinalIgnoreCase)) - ?.Version ?? new Version(0, 0, 0, 0)) >= new Version(1, 3, 0, 10); - try - { - var version = _glamourerApiVersions.Invoke(); - if (version is { Major: 1, Minor: >= 1 } && versionValid) - { - apiAvailable = true; - } - } - catch - { - // ignore - } - _shownGlamourerUnavailable = _shownGlamourerUnavailable && !apiAvailable; - - APIAvailable = apiAvailable; - } - catch - { - APIAvailable = apiAvailable; - } - finally - { - if (!apiAvailable && !_shownGlamourerUnavailable) - { - _shownGlamourerUnavailable = true; - _lightlessMediator.Publish(new NotificationMessage("Glamourer inactive", "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Lightless. If you just updated Glamourer, ignore this message.", - NotificationType.Error)); - } - } - } - public async Task ApplyAllAsync(ILogger logger, GameObjectHandler handler, string? customization, Guid applicationId, CancellationToken token, bool fireAndForget = false) { if (!APIAvailable || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return; @@ -210,6 +171,49 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC } } + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + var version = _glamourerApiVersions.Invoke(); + return version is { Major: 1, Minor: >= 1 } + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Glamourer API version"); + return IpcConnectionState.Error; + } + } + + protected override void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current) + { + base.OnConnectionStateChanged(previous, current); + + if (current == IpcConnectionState.Available) + { + _shownGlamourerUnavailable = false; + return; + } + + if (_shownGlamourerUnavailable || current == IpcConnectionState.Unknown) + { + return; + } + + _shownGlamourerUnavailable = true; + _lightlessMediator.Publish(new NotificationMessage("Glamourer inactive", + "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Lightless. If you just updated Glamourer, ignore this message.", + NotificationType.Error)); + } + private void GlamourerChanged(nint address) { _lightlessMediator.Publish(new GlamourerChangedMessage(address)); diff --git a/LightlessSync/Interop/Ipc/IpcCallerHeels.cs b/LightlessSync/Interop/Ipc/IpcCallerHeels.cs index 69b359c..23fe192 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerHeels.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerHeels.cs @@ -1,13 +1,16 @@ using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerHeels : IIpcCaller +public sealed class IpcCallerHeels : IpcServiceBase { + private static readonly IpcServiceDescriptor HeelsDescriptor = new("SimpleHeels", "Simple Heels", new Version(0, 0, 0, 0)); + private readonly ILogger _logger; private readonly LightlessMediator _lightlessMediator; private readonly DalamudUtilService _dalamudUtil; @@ -18,6 +21,7 @@ public sealed class IpcCallerHeels : IIpcCaller private readonly ICallGateSubscriber _heelsUnregisterPlayer; public IpcCallerHeels(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, LightlessMediator lightlessMediator) + : base(logger, lightlessMediator, pi, HeelsDescriptor) { _logger = logger; _lightlessMediator = lightlessMediator; @@ -32,8 +36,26 @@ public sealed class IpcCallerHeels : IIpcCaller CheckAPI(); } + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } - public bool APIAvailable { get; private set; } = false; + try + { + return _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 1 } + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query SimpleHeels API version"); + return IpcConnectionState.Error; + } + } private void HeelsOffsetChange(string offset) { @@ -74,20 +96,14 @@ public sealed class IpcCallerHeels : IIpcCaller }).ConfigureAwait(false); } - public void CheckAPI() + protected override void Dispose(bool disposing) { - try + base.Dispose(disposing); + if (!disposing) { - APIAvailable = _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 1 }; + return; } - catch - { - APIAvailable = false; - } - } - public void Dispose() - { _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerHonorific.cs b/LightlessSync/Interop/Ipc/IpcCallerHonorific.cs index 58588c5..4a2ed5d 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerHonorific.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerHonorific.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; @@ -8,8 +9,10 @@ using System.Text; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerHonorific : IIpcCaller +public sealed class IpcCallerHonorific : IpcServiceBase { + private static readonly IpcServiceDescriptor HonorificDescriptor = new("Honorific", "Honorific", new Version(0, 0, 0, 0)); + private readonly ICallGateSubscriber<(uint major, uint minor)> _honorificApiVersion; private readonly ICallGateSubscriber _honorificClearCharacterTitle; private readonly ICallGateSubscriber _honorificDisposing; @@ -22,7 +25,7 @@ public sealed class IpcCallerHonorific : IIpcCaller private readonly DalamudUtilService _dalamudUtil; public IpcCallerHonorific(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, - LightlessMediator lightlessMediator) + LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, HonorificDescriptor) { _logger = logger; _lightlessMediator = lightlessMediator; @@ -41,23 +44,14 @@ public sealed class IpcCallerHonorific : IIpcCaller CheckAPI(); } - - public bool APIAvailable { get; private set; } = false; - - public void CheckAPI() + protected override void Dispose(bool disposing) { - try + base.Dispose(disposing); + if (!disposing) { - APIAvailable = _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 1 }; + return; } - catch - { - APIAvailable = false; - } - } - public void Dispose() - { _honorificLocalCharacterTitleChanged.Unsubscribe(OnHonorificLocalCharacterTitleChanged); _honorificDisposing.Unsubscribe(OnHonorificDisposing); _honorificReady.Unsubscribe(OnHonorificReady); @@ -113,6 +107,27 @@ public sealed class IpcCallerHonorific : IIpcCaller } } + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + return _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 1 } + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Honorific API version"); + return IpcConnectionState.Error; + } + } + private void OnHonorificDisposing() { _lightlessMediator.Publish(new HonorificMessage(string.Empty)); diff --git a/LightlessSync/Interop/Ipc/IpcCallerMoodles.cs b/LightlessSync/Interop/Ipc/IpcCallerMoodles.cs index 610ece4..e8b1b76 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerMoodles.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerMoodles.cs @@ -1,14 +1,17 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerMoodles : IIpcCaller +public sealed class IpcCallerMoodles : IpcServiceBase { + private static readonly IpcServiceDescriptor MoodlesDescriptor = new("Moodles", "Moodles", new Version(0, 0, 0, 0)); + private readonly ICallGateSubscriber _moodlesApiVersion; private readonly ICallGateSubscriber _moodlesOnChange; private readonly ICallGateSubscriber _moodlesGetStatus; @@ -19,7 +22,7 @@ public sealed class IpcCallerMoodles : IIpcCaller private readonly LightlessMediator _lightlessMediator; public IpcCallerMoodles(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, - LightlessMediator lightlessMediator) + LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, MoodlesDescriptor) { _logger = logger; _dalamudUtil = dalamudUtil; @@ -41,22 +44,14 @@ public sealed class IpcCallerMoodles : IIpcCaller _lightlessMediator.Publish(new MoodlesMessage(character.Address)); } - public bool APIAvailable { get; private set; } = false; - - public void CheckAPI() + protected override void Dispose(bool disposing) { - try + base.Dispose(disposing); + if (!disposing) { - APIAvailable = _moodlesApiVersion.InvokeFunc() == 3; + return; } - catch - { - APIAvailable = false; - } - } - public void Dispose() - { _moodlesOnChange.Unsubscribe(OnMoodlesChange); } @@ -101,4 +96,25 @@ public sealed class IpcCallerMoodles : IIpcCaller _logger.LogWarning(e, "Could not Set Moodles Status"); } } + + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + return _moodlesApiVersion.InvokeFunc() == 3 + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Moodles API version"); + return IpcConnectionState.Error; + } + } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs b/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs index 4135642..4169e5c 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerPenumbra.cs @@ -1,4 +1,6 @@ -using Dalamud.Plugin; +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.Interop.Ipc.Penumbra; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Handlers; using LightlessSync.Services; @@ -8,520 +10,210 @@ using Microsoft.Extensions.Logging; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using Penumbra.Api.IpcSubscribers; -using System.Collections.Concurrent; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCaller +public sealed class IpcCallerPenumbra : IpcServiceBase { - private readonly IDalamudPluginInterface _pi; - private readonly DalamudUtilService _dalamudUtil; - private readonly LightlessMediator _lightlessMediator; - private readonly RedrawManager _redrawManager; - private readonly ActorObjectService _actorObjectService; - private bool _shownPenumbraUnavailable = false; - private string? _penumbraModDirectory; - public string? ModDirectory - { - get => _penumbraModDirectory; - private set - { - if (!string.Equals(_penumbraModDirectory, value, StringComparison.Ordinal)) - { - _penumbraModDirectory = value; - _lightlessMediator.Publish(new PenumbraDirectoryChangedMessage(_penumbraModDirectory)); - } - } - } + private static readonly IpcServiceDescriptor PenumbraDescriptor = new("Penumbra", "Penumbra", new Version(1, 2, 0, 22)); - private readonly ConcurrentDictionary _penumbraRedrawRequests = new(); - private readonly ConcurrentDictionary _trackedActors = new(); + private readonly PenumbraCollections _collections; + private readonly PenumbraResource _resources; + private readonly PenumbraRedraw _redraw; + private readonly PenumbraTexture _textures; - private readonly EventSubscriber _penumbraDispose; - private readonly EventSubscriber _penumbraGameObjectResourcePathResolved; - private readonly EventSubscriber _penumbraInit; - private readonly EventSubscriber _penumbraModSettingChanged; - private readonly EventSubscriber _penumbraObjectIsRedrawn; - - private readonly AddTemporaryMod _penumbraAddTemporaryMod; - private readonly AssignTemporaryCollection _penumbraAssignTemporaryCollection; - private readonly ConvertTextureFile _penumbraConvertTextureFile; - private readonly CreateTemporaryCollection _penumbraCreateNamedTemporaryCollection; private readonly GetEnabledState _penumbraEnabled; - private readonly GetPlayerMetaManipulations _penumbraGetMetaManipulations; - private readonly RedrawObject _penumbraRedraw; - private readonly DeleteTemporaryCollection _penumbraRemoveTemporaryCollection; - private readonly RemoveTemporaryMod _penumbraRemoveTemporaryMod; - private readonly GetModDirectory _penumbraResolveModDir; - private readonly ResolvePlayerPathsAsync _penumbraResolvePaths; - private readonly GetGameObjectResourcePaths _penumbraResourcePaths; - //private readonly GetPlayerResourcePaths _penumbraPlayerResourcePaths; - private readonly GetCollections _penumbraGetCollections; - private readonly ConcurrentDictionary _activeTemporaryCollections = new(); - private int _performedInitialCleanup; + private readonly GetModDirectory _penumbraGetModDirectory; + private readonly EventSubscriber _penumbraInit; + private readonly EventSubscriber _penumbraDispose; + private readonly EventSubscriber _penumbraModSettingChanged; - public IpcCallerPenumbra(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, - LightlessMediator lightlessMediator, RedrawManager redrawManager, ActorObjectService actorObjectService) : base(logger, lightlessMediator) + private bool _shownPenumbraUnavailable; + private string? _modDirectory; + + public IpcCallerPenumbra( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator, + RedrawManager redrawManager, + ActorObjectService actorObjectService) : base(logger, mediator, pluginInterface, PenumbraDescriptor) { - _pi = pi; - _dalamudUtil = dalamudUtil; - _lightlessMediator = lightlessMediator; - _redrawManager = redrawManager; - _actorObjectService = actorObjectService; - _penumbraInit = Initialized.Subscriber(pi, PenumbraInit); - _penumbraDispose = Disposed.Subscriber(pi, PenumbraDispose); - _penumbraResolveModDir = new GetModDirectory(pi); - _penumbraRedraw = new RedrawObject(pi); - _penumbraObjectIsRedrawn = GameObjectRedrawn.Subscriber(pi, RedrawEvent); - _penumbraGetMetaManipulations = new GetPlayerMetaManipulations(pi); - _penumbraRemoveTemporaryMod = new RemoveTemporaryMod(pi); - _penumbraAddTemporaryMod = new AddTemporaryMod(pi); - _penumbraCreateNamedTemporaryCollection = new CreateTemporaryCollection(pi); - _penumbraRemoveTemporaryCollection = new DeleteTemporaryCollection(pi); - _penumbraAssignTemporaryCollection = new AssignTemporaryCollection(pi); - _penumbraGetCollections = new GetCollections(pi); - _penumbraResolvePaths = new ResolvePlayerPathsAsync(pi); - _penumbraEnabled = new GetEnabledState(pi); - _penumbraModSettingChanged = ModSettingChanged.Subscriber(pi, (change, arg1, arg, b) => - { - if (change == ModSettingChange.EnableState) - _lightlessMediator.Publish(new PenumbraModSettingChangedMessage()); - }); - _penumbraConvertTextureFile = new ConvertTextureFile(pi); - _penumbraResourcePaths = new GetGameObjectResourcePaths(pi); - //_penumbraPlayerResourcePaths = new GetPlayerResourcePaths(pi); + _penumbraEnabled = new GetEnabledState(pluginInterface); + _penumbraGetModDirectory = new GetModDirectory(pluginInterface); + _penumbraInit = Initialized.Subscriber(pluginInterface, HandlePenumbraInitialized); + _penumbraDispose = Disposed.Subscriber(pluginInterface, HandlePenumbraDisposed); + _penumbraModSettingChanged = ModSettingChanged.Subscriber(pluginInterface, HandlePenumbraModSettingChanged); - _penumbraGameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded); + _collections = RegisterInterop(new PenumbraCollections(logger, pluginInterface, dalamudUtil, mediator)); + _resources = RegisterInterop(new PenumbraResource(logger, pluginInterface, dalamudUtil, mediator, actorObjectService)); + _redraw = RegisterInterop(new PenumbraRedraw(logger, pluginInterface, dalamudUtil, mediator, redrawManager)); + _textures = RegisterInterop(new PenumbraTexture(logger, pluginInterface, dalamudUtil, mediator, _redraw)); + + SubscribeMediatorEvents(); CheckAPI(); CheckModDirectory(); - - Mediator.Subscribe(this, (msg) => - { - _penumbraRedraw.Invoke(msg.Character.ObjectIndex, RedrawType.AfterGPose); - }); - - Mediator.Subscribe(this, (msg) => _shownPenumbraUnavailable = false); - - Mediator.Subscribe(this, msg => - { - if (msg.Descriptor.Address != nint.Zero) - { - _trackedActors[(IntPtr)msg.Descriptor.Address] = 0; - } - }); - - Mediator.Subscribe(this, msg => - { - if (msg.Descriptor.Address != nint.Zero) - { - _trackedActors.TryRemove((IntPtr)msg.Descriptor.Address, out _); - } - }); - - Mediator.Subscribe(this, msg => - { - if (msg.GameObjectHandler.Address != nint.Zero) - { - _trackedActors[(IntPtr)msg.GameObjectHandler.Address] = 0; - } - }); - - Mediator.Subscribe(this, msg => - { - if (msg.GameObjectHandler.Address != nint.Zero) - { - _trackedActors.TryRemove((IntPtr)msg.GameObjectHandler.Address, out _); - } - }); - - foreach (var descriptor in _actorObjectService.PlayerDescriptors) - { - if (descriptor.Address != nint.Zero) - { - _trackedActors[(IntPtr)descriptor.Address] = 0; - } - } } - public bool APIAvailable { get; private set; } = false; - - public void CheckAPI() + public string? ModDirectory { - bool penumbraAvailable = false; - try + get => _modDirectory; + private set { - var penumbraVersion = (_pi.InstalledPlugins - .FirstOrDefault(p => string.Equals(p.InternalName, "Penumbra", StringComparison.OrdinalIgnoreCase)) - ?.Version ?? new Version(0, 0, 0, 0)); - penumbraAvailable = penumbraVersion >= new Version(1, 2, 0, 22); - try + if (string.Equals(_modDirectory, value, StringComparison.Ordinal)) { - penumbraAvailable &= _penumbraEnabled.Invoke(); + return; } - catch - { - penumbraAvailable = false; - } - _shownPenumbraUnavailable = _shownPenumbraUnavailable && !penumbraAvailable; - APIAvailable = penumbraAvailable; - } - catch - { - APIAvailable = penumbraAvailable; - } - finally - { - if (!penumbraAvailable && !_shownPenumbraUnavailable) - { - _shownPenumbraUnavailable = true; - _lightlessMediator.Publish(new NotificationMessage("Penumbra inactive", - "Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Lightless. If you just updated Penumbra, ignore this message.", - NotificationType.Error)); - } - } - if (APIAvailable) - { - ScheduleTemporaryCollectionCleanup(); + _modDirectory = value; + Mediator.Publish(new PenumbraDirectoryChangedMessage(_modDirectory)); } } + public Task AssignTemporaryCollectionAsync(ILogger logger, Guid collectionId, int objectIndex) + => _collections.AssignTemporaryCollectionAsync(logger, collectionId, objectIndex); + + public Task CreateTemporaryCollectionAsync(ILogger logger, string uid) + => _collections.CreateTemporaryCollectionAsync(logger, uid); + + public Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collectionId) + => _collections.RemoveTemporaryCollectionAsync(logger, applicationId, collectionId); + + public Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary modPaths) + => _collections.SetTemporaryModsAsync(logger, applicationId, collectionId, modPaths); + + public Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData) + => _collections.SetManipulationDataAsync(logger, applicationId, collectionId, manipulationData); + + public Task>?> GetCharacterData(ILogger logger, GameObjectHandler handler) + => _resources.GetCharacterDataAsync(logger, handler); + + public string GetMetaManipulations() + => _resources.GetMetaManipulations(); + + public Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse) + => _resources.ResolvePathsAsync(forward, reverse); + + public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token) + => _redraw.RedrawAsync(logger, handler, applicationId, token); + + public Task ConvertTextureFiles(ILogger logger, IReadOnlyList jobs, IProgress? progress, CancellationToken token) + => _textures.ConvertTextureFilesAsync(logger, jobs, progress, token); + + public Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token) + => _textures.ConvertTextureFileDirectAsync(job, token); + public void CheckModDirectory() { if (!APIAvailable) { ModDirectory = string.Empty; - } - else - { - ModDirectory = _penumbraResolveModDir!.Invoke().ToLowerInvariant(); - } - } - - private void ScheduleTemporaryCollectionCleanup() - { - if (Interlocked.Exchange(ref _performedInitialCleanup, 1) != 0) - return; - - _ = Task.Run(CleanupTemporaryCollectionsAsync); - } - - private async Task CleanupTemporaryCollectionsAsync() - { - if (!APIAvailable) return; + } try { - var collections = await _dalamudUtil.RunOnFrameworkThread(() => _penumbraGetCollections.Invoke()).ConfigureAwait(false); - foreach (var (collectionId, name) in collections) - { - if (!IsLightlessCollectionName(name)) - continue; - - if (_activeTemporaryCollections.ContainsKey(collectionId)) - continue; - - Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId); - var deleteResult = await _dalamudUtil.RunOnFrameworkThread(() => - { - var result = (PenumbraApiEc)_penumbraRemoveTemporaryCollection.Invoke(collectionId); - Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result); - return result; - }).ConfigureAwait(false); - if (deleteResult == PenumbraApiEc.Success) - { - _activeTemporaryCollections.TryRemove(collectionId, out _); - } - else - { - Logger.LogDebug("Skipped removing temporary collection {CollectionName} ({CollectionId}). Result: {Result}", name, collectionId, deleteResult); - } - } + ModDirectory = _penumbraGetModDirectory.Invoke().ToLowerInvariant(); } catch (Exception ex) { - Logger.LogWarning(ex, "Failed to clean up Penumbra temporary collections"); + Logger.LogWarning(ex, "Failed to resolve Penumbra mod directory"); } } - private static bool IsLightlessCollectionName(string? name) - => !string.IsNullOrEmpty(name) && name.StartsWith("Lightless_", StringComparison.Ordinal); + protected override bool IsPluginEnabled() + { + try + { + return _penumbraEnabled.Invoke(); + } + catch + { + return false; + } + } + + protected override void OnConnectionStateChanged(IpcConnectionState previous, IpcConnectionState current) + { + base.OnConnectionStateChanged(previous, current); + + if (current == IpcConnectionState.Available) + { + _shownPenumbraUnavailable = false; + if (string.IsNullOrEmpty(ModDirectory)) + { + CheckModDirectory(); + } + return; + } + + ModDirectory = string.Empty; + _redraw.CancelPendingRedraws(); + + if (_shownPenumbraUnavailable || current == IpcConnectionState.Unknown) + { + return; + } + + _shownPenumbraUnavailable = true; + Mediator.Publish(new NotificationMessage( + "Penumbra inactive", + "Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Lightless. If you just updated Penumbra, ignore this message.", + NotificationType.Error)); + } + + private void SubscribeMediatorEvents() + { + Mediator.Subscribe(this, msg => + { + _redraw.RequestImmediateRedraw(msg.Character.ObjectIndex, RedrawType.AfterGPose); + }); + + Mediator.Subscribe(this, _ => _shownPenumbraUnavailable = false); + + Mediator.Subscribe(this, msg => _resources.TrackActor(msg.Descriptor.Address)); + Mediator.Subscribe(this, msg => _resources.UntrackActor(msg.Descriptor.Address)); + Mediator.Subscribe(this, msg => _resources.TrackActor(msg.GameObjectHandler.Address)); + Mediator.Subscribe(this, msg => _resources.UntrackActor(msg.GameObjectHandler.Address)); + } + + private void HandlePenumbraInitialized() + { + Mediator.Publish(new PenumbraInitializedMessage()); + CheckModDirectory(); + _redraw.RequestImmediateRedraw(0, RedrawType.Redraw); + CheckAPI(); + } + + private void HandlePenumbraDisposed() + { + _redraw.CancelPendingRedraws(); + ModDirectory = string.Empty; + Mediator.Publish(new PenumbraDisposedMessage()); + CheckAPI(); + } + + private void HandlePenumbraModSettingChanged(ModSettingChange change, Guid _, string __, bool ___) + { + if (change == ModSettingChange.EnableState) + { + Mediator.Publish(new PenumbraModSettingChangedMessage()); + CheckAPI(); + } + } protected override void Dispose(bool disposing) { base.Dispose(disposing); - _redrawManager.Cancel(); + if (!disposing) + { + return; + } _penumbraModSettingChanged.Dispose(); - _penumbraGameObjectResourcePathResolved.Dispose(); _penumbraDispose.Dispose(); _penumbraInit.Dispose(); - _penumbraObjectIsRedrawn.Dispose(); - } - - public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collName, int idx) - { - if (!APIAvailable) return; - - await _dalamudUtil.RunOnFrameworkThread(() => - { - var retAssign = _penumbraAssignTemporaryCollection.Invoke(collName, idx, forceAssignment: true); - logger.LogTrace("Assigning Temp Collection {collName} to index {idx}, Success: {ret}", collName, idx, retAssign); - return collName; - }).ConfigureAwait(false); - } - - public async Task ConvertTextureFiles(ILogger logger, IReadOnlyList jobs, IProgress? progress, CancellationToken token) - { - if (!APIAvailable || jobs.Count == 0) - { - return; - } - - _lightlessMediator.Publish(new HaltScanMessage(nameof(ConvertTextureFiles))); - - var totalJobs = jobs.Count; - var completedJobs = 0; - - try - { - foreach (var job in jobs) - { - if (token.IsCancellationRequested) - { - break; - } - - progress?.Report(new TextureConversionProgress(completedJobs, totalJobs, job)); - - logger.LogInformation("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType); - var convertTask = _penumbraConvertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps); - await convertTask.ConfigureAwait(false); - - if (convertTask.IsCompletedSuccessfully && job.DuplicateTargets is { Count: > 0 }) - { - foreach (var duplicate in job.DuplicateTargets) - { - logger.LogInformation("Synchronizing duplicate {Duplicate}", duplicate); - try - { - File.Copy(job.OutputFile, duplicate, overwrite: true); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to copy duplicate {Duplicate}", duplicate); - } - } - } - - completedJobs++; - } - } - finally - { - _lightlessMediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFiles))); - } - - if (completedJobs > 0 && !token.IsCancellationRequested) - { - await _dalamudUtil.RunOnFrameworkThread(async () => - { - var player = await _dalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false); - if (player == null) - { - return; - } - - var gameObject = await _dalamudUtil.CreateGameObjectAsync(player).ConfigureAwait(false); - _penumbraRedraw.Invoke(gameObject!.ObjectIndex, setting: RedrawType.Redraw); - }).ConfigureAwait(false); - } - } - - public async Task CreateTemporaryCollectionAsync(ILogger logger, string uid) - { - if (!APIAvailable) return Guid.Empty; - - var (collectionId, collectionName) = await _dalamudUtil.RunOnFrameworkThread(() => - { - var collName = "Lightless_" + uid; - _penumbraCreateNamedTemporaryCollection.Invoke(collName, collName, out var collId); - logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId); - return (collId, collName); - - }).ConfigureAwait(false); - if (collectionId != Guid.Empty) - { - _activeTemporaryCollections[collectionId] = collectionName; - } - - return collectionId; - } - - public async Task>?> GetCharacterData(ILogger logger, GameObjectHandler handler) - { - if (!APIAvailable) return null; - - return await _dalamudUtil.RunOnFrameworkThread(() => - { - logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths"); - var idx = handler.GetGameObject()?.ObjectIndex; - if (idx == null) return null; - return _penumbraResourcePaths.Invoke(idx.Value)[0]; - }).ConfigureAwait(false); - } - - public string GetMetaManipulations() - { - if (!APIAvailable) return string.Empty; - return _penumbraGetMetaManipulations.Invoke(); - } - - public async Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token) - { - if (!APIAvailable || _dalamudUtil.IsZoning) return; - try - { - await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false); - await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) => - { - logger.LogDebug("[{appid}] Calling on IPC: PenumbraRedraw", applicationId); - _penumbraRedraw!.Invoke(chara.ObjectIndex, setting: RedrawType.Redraw); - - }, token).ConfigureAwait(false); - } - finally - { - _redrawManager.RedrawSemaphore.Release(); - } - } - - public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collId) - { - if (!APIAvailable) return; - await _dalamudUtil.RunOnFrameworkThread(() => - { - logger.LogTrace("[{applicationId}] Removing temp collection for {collId}", applicationId, collId); - var ret2 = _penumbraRemoveTemporaryCollection.Invoke(collId); - logger.LogTrace("[{applicationId}] RemoveTemporaryCollection: {ret2}", applicationId, ret2); - }).ConfigureAwait(false); - if (collId != Guid.Empty) - { - _activeTemporaryCollections.TryRemove(collId, out _); - } - } - - public async Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse) - { - return await _penumbraResolvePaths.Invoke(forward, reverse).ConfigureAwait(false); - } - - public async Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token) - { - if (!APIAvailable) return; - - token.ThrowIfCancellationRequested(); - - await _penumbraConvertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps) - .ConfigureAwait(false); - - if (job.DuplicateTargets is { Count: > 0 }) - { - foreach (var duplicate in job.DuplicateTargets) - { - try - { - File.Copy(job.OutputFile, duplicate, overwrite: true); - } - catch (Exception ex) - { - Logger.LogDebug(ex, "Failed to copy duplicate {Duplicate} for texture conversion", duplicate); - } - } - } - } - - public async Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collId, string manipulationData) - { - if (!APIAvailable) return; - - await _dalamudUtil.RunOnFrameworkThread(() => - { - logger.LogTrace("[{applicationId}] Manip: {data}", applicationId, manipulationData); - var retAdd = _penumbraAddTemporaryMod.Invoke("LightlessChara_Meta", collId, [], manipulationData, 0); - logger.LogTrace("[{applicationId}] Setting temp meta mod for {collId}, Success: {ret}", applicationId, collId, retAdd); - }).ConfigureAwait(false); - } - - public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collId, Dictionary modPaths) - { - if (!APIAvailable) return; - - await _dalamudUtil.RunOnFrameworkThread(() => - { - foreach (var mod in modPaths) - { - logger.LogTrace("[{applicationId}] Change: {from} => {to}", applicationId, mod.Key, mod.Value); - } - var retRemove = _penumbraRemoveTemporaryMod.Invoke("LightlessChara_Files", collId, 0); - logger.LogTrace("[{applicationId}] Removing temp files mod for {collId}, Success: {ret}", applicationId, collId, retRemove); - var retAdd = _penumbraAddTemporaryMod.Invoke("LightlessChara_Files", collId, modPaths, string.Empty, 0); - logger.LogTrace("[{applicationId}] Setting temp files mod for {collId}, Success: {ret}", applicationId, collId, retAdd); - }).ConfigureAwait(false); - } - - private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) - { - bool wasRequested = false; - if (_penumbraRedrawRequests.TryGetValue(objectAddress, out var redrawRequest) && redrawRequest) - { - _penumbraRedrawRequests[objectAddress] = false; - } - else - { - _lightlessMediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, wasRequested)); - } - } - - private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) - { - if (ptr == IntPtr.Zero) - return; - - if (!_trackedActors.ContainsKey(ptr)) - { - var descriptor = _actorObjectService.PlayerDescriptors.FirstOrDefault(d => d.Address == ptr); - if (descriptor.Address != nint.Zero) - { - _trackedActors[ptr] = 0; - } - else - { - return; - } - } - - if (string.Compare(arg1, arg2, ignoreCase: true, System.Globalization.CultureInfo.InvariantCulture) == 0) - return; - - _lightlessMediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2)); - } - - private void PenumbraDispose() - { - _redrawManager.Cancel(); - _lightlessMediator.Publish(new PenumbraDisposedMessage()); - } - - private void PenumbraInit() - { - APIAvailable = true; - ModDirectory = _penumbraResolveModDir.Invoke(); - _lightlessMediator.Publish(new PenumbraInitializedMessage()); - ScheduleTemporaryCollectionCleanup(); - _penumbraRedraw!.Invoke(0, setting: RedrawType.Redraw); } } diff --git a/LightlessSync/Interop/Ipc/IpcCallerPetNames.cs b/LightlessSync/Interop/Ipc/IpcCallerPetNames.cs index 9839f29..5d7fea9 100644 --- a/LightlessSync/Interop/Ipc/IpcCallerPetNames.cs +++ b/LightlessSync/Interop/Ipc/IpcCallerPetNames.cs @@ -1,14 +1,17 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; namespace LightlessSync.Interop.Ipc; -public sealed class IpcCallerPetNames : IIpcCaller +public sealed class IpcCallerPetNames : IpcServiceBase { + private static readonly IpcServiceDescriptor PetRenamerDescriptor = new("PetRenamer", "Pet Renamer", new Version(0, 0, 0, 0)); + private readonly ILogger _logger; private readonly DalamudUtilService _dalamudUtil; private readonly LightlessMediator _lightlessMediator; @@ -24,7 +27,7 @@ public sealed class IpcCallerPetNames : IIpcCaller private readonly ICallGateSubscriber _clearPlayerData; public IpcCallerPetNames(ILogger logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, - LightlessMediator lightlessMediator) + LightlessMediator lightlessMediator) : base(logger, lightlessMediator, pi, PetRenamerDescriptor) { _logger = logger; _dalamudUtil = dalamudUtil; @@ -46,25 +49,6 @@ public sealed class IpcCallerPetNames : IIpcCaller CheckAPI(); } - - public bool APIAvailable { get; private set; } = false; - - public void CheckAPI() - { - try - { - APIAvailable = _enabled?.InvokeFunc() ?? false; - if (APIAvailable) - { - APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 4, Item2: >= 0 }; - } - } - catch - { - APIAvailable = false; - } - } - private void OnPetNicknamesReady() { CheckAPI(); @@ -76,6 +60,34 @@ public sealed class IpcCallerPetNames : IIpcCaller _lightlessMediator.Publish(new PetNamesMessage(string.Empty)); } + protected override IpcConnectionState EvaluateState() + { + var state = base.EvaluateState(); + if (state != IpcConnectionState.Available) + { + return state; + } + + try + { + var enabled = _enabled?.InvokeFunc() ?? false; + if (!enabled) + { + return IpcConnectionState.PluginDisabled; + } + + var version = _apiVersion?.InvokeFunc() ?? (0u, 0u); + return version.Item1 == 4 && version.Item2 >= 0 + ? IpcConnectionState.Available + : IpcConnectionState.VersionMismatch; + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Failed to query Pet Renamer API version"); + return IpcConnectionState.Error; + } + } + public string GetLocalNames() { if (!APIAvailable) return string.Empty; @@ -149,8 +161,14 @@ public sealed class IpcCallerPetNames : IIpcCaller _lightlessMediator.Publish(new PetNamesMessage(data)); } - public void Dispose() + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + if (!disposing) + { + return; + } + _petnamesReady.Unsubscribe(OnPetNicknamesReady); _petnamesDisposing.Unsubscribe(OnPetNicknamesDispose); _playerDataChanged.Unsubscribe(OnLocalPetNicknamesDataChange); diff --git a/LightlessSync/Interop/Ipc/IpcProvider.cs b/LightlessSync/Interop/Ipc/IpcProvider.cs index 88e0202..77f8043 100644 --- a/LightlessSync/Interop/Ipc/IpcProvider.cs +++ b/LightlessSync/Interop/Ipc/IpcProvider.cs @@ -1,4 +1,5 @@ -using Dalamud.Game.ClientState.Objects.Types; +using System; +using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using LightlessSync.PlayerData.Handlers; @@ -14,9 +15,7 @@ public class IpcProvider : IHostedService, IMediatorSubscriber private readonly ILogger _logger; private readonly IDalamudPluginInterface _pi; private readonly CharaDataManager _charaDataManager; - private ICallGateProvider? _loadFileProvider; - private ICallGateProvider>? _loadFileAsyncProvider; - private ICallGateProvider>? _handledGameAddresses; + private readonly List _ipcRegisters = []; private readonly List _activeGameObjectHandlers = []; public LightlessMediator Mediator { get; init; } @@ -44,12 +43,9 @@ public class IpcProvider : IHostedService, IMediatorSubscriber public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting IpcProviderService"); - _loadFileProvider = _pi.GetIpcProvider("LightlessSync.LoadMcdf"); - _loadFileProvider.RegisterFunc(LoadMcdf); - _loadFileAsyncProvider = _pi.GetIpcProvider>("LightlessSync.LoadMcdfAsync"); - _loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync); - _handledGameAddresses = _pi.GetIpcProvider>("LightlessSync.GetHandledAddresses"); - _handledGameAddresses.RegisterFunc(GetHandledAddresses); + _ipcRegisters.Add(RegisterFunc("LightlessSync.LoadMcdf", LoadMcdf)); + _ipcRegisters.Add(RegisterFunc>("LightlessSync.LoadMcdfAsync", LoadMcdfAsync)); + _ipcRegisters.Add(RegisterFunc("LightlessSync.GetHandledAddresses", GetHandledAddresses)); _logger.LogInformation("Started IpcProviderService"); return Task.CompletedTask; } @@ -57,9 +53,11 @@ public class IpcProvider : IHostedService, IMediatorSubscriber public Task StopAsync(CancellationToken cancellationToken) { _logger.LogDebug("Stopping IpcProvider Service"); - _loadFileProvider?.UnregisterFunc(); - _loadFileAsyncProvider?.UnregisterFunc(); - _handledGameAddresses?.UnregisterFunc(); + foreach (var register in _ipcRegisters) + { + register.Dispose(); + } + _ipcRegisters.Clear(); Mediator.UnsubscribeAll(this); return Task.CompletedTask; } @@ -89,4 +87,40 @@ public class IpcProvider : IHostedService, IMediatorSubscriber { return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList(); } + + private IpcRegister RegisterFunc(string label, Func> handler) + { + var provider = _pi.GetIpcProvider>(label); + provider.RegisterFunc(handler); + return new IpcRegister(provider.UnregisterFunc); + } + + private IpcRegister RegisterFunc(string label, Func handler) + { + var provider = _pi.GetIpcProvider(label); + provider.RegisterFunc(handler); + return new IpcRegister(provider.UnregisterFunc); + } + + private sealed class IpcRegister : IDisposable + { + private readonly Action _unregister; + private bool _disposed; + + public IpcRegister(Action unregister) + { + _unregister = unregister; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _unregister(); + _disposed = true; + } + } } diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs new file mode 100644 index 0000000..4f7b000 --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraBase.cs @@ -0,0 +1,27 @@ +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public abstract class PenumbraBase : IpcInteropBase +{ + protected PenumbraBase( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator) : base(logger) + { + PluginInterface = pluginInterface; + DalamudUtil = dalamudUtil; + Mediator = mediator; + } + + protected IDalamudPluginInterface PluginInterface { get; } + + protected DalamudUtilService DalamudUtil { get; } + + protected LightlessMediator Mediator { get; } +} diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs new file mode 100644 index 0000000..e5c28e2 --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraCollections.cs @@ -0,0 +1,197 @@ +using System.Collections.Concurrent; +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Enums; +using Penumbra.Api.IpcSubscribers; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public sealed class PenumbraCollections : PenumbraBase +{ + private readonly CreateTemporaryCollection _createNamedTemporaryCollection; + private readonly AssignTemporaryCollection _assignTemporaryCollection; + private readonly DeleteTemporaryCollection _removeTemporaryCollection; + private readonly AddTemporaryMod _addTemporaryMod; + private readonly RemoveTemporaryMod _removeTemporaryMod; + private readonly GetCollections _getCollections; + private readonly ConcurrentDictionary _activeTemporaryCollections = new(); + + private int _cleanupScheduled; + + public PenumbraCollections( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator) : base(logger, pluginInterface, dalamudUtil, mediator) + { + _createNamedTemporaryCollection = new CreateTemporaryCollection(pluginInterface); + _assignTemporaryCollection = new AssignTemporaryCollection(pluginInterface); + _removeTemporaryCollection = new DeleteTemporaryCollection(pluginInterface); + _addTemporaryMod = new AddTemporaryMod(pluginInterface); + _removeTemporaryMod = new RemoveTemporaryMod(pluginInterface); + _getCollections = new GetCollections(pluginInterface); + } + + public override string Name => "Penumbra.Collections"; + + public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collectionId, int objectIndex) + { + if (!IsAvailable || collectionId == Guid.Empty) + { + return; + } + + await DalamudUtil.RunOnFrameworkThread(() => + { + var result = _assignTemporaryCollection.Invoke(collectionId, objectIndex, forceAssignment: true); + logger.LogTrace("Assigning Temp Collection {CollectionId} to index {ObjectIndex}, Success: {Result}", collectionId, objectIndex, result); + return result; + }).ConfigureAwait(false); + } + + public async Task CreateTemporaryCollectionAsync(ILogger logger, string uid) + { + if (!IsAvailable) + { + return Guid.Empty; + } + + var (collectionId, collectionName) = await DalamudUtil.RunOnFrameworkThread(() => + { + var name = $"Lightless_{uid}"; + _createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId); + logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}", name, tempCollectionId); + return (tempCollectionId, name); + }).ConfigureAwait(false); + + if (collectionId != Guid.Empty) + { + _activeTemporaryCollections[collectionId] = collectionName; + } + + return collectionId; + } + + public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collectionId) + { + if (!IsAvailable || collectionId == Guid.Empty) + { + return; + } + + await DalamudUtil.RunOnFrameworkThread(() => + { + logger.LogTrace("[{ApplicationId}] Removing temp collection for {CollectionId}", applicationId, collectionId); + var result = _removeTemporaryCollection.Invoke(collectionId); + logger.LogTrace("[{ApplicationId}] RemoveTemporaryCollection: {Result}", applicationId, result); + }).ConfigureAwait(false); + + _activeTemporaryCollections.TryRemove(collectionId, out _); + } + + public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, IReadOnlyDictionary modPaths) + { + if (!IsAvailable || collectionId == Guid.Empty) + { + return; + } + + await DalamudUtil.RunOnFrameworkThread(() => + { + foreach (var mod in modPaths) + { + logger.LogTrace("[{ApplicationId}] Change: {From} => {To}", applicationId, mod.Key, mod.Value); + } + + var removeResult = _removeTemporaryMod.Invoke("LightlessChara_Files", collectionId, 0); + logger.LogTrace("[{ApplicationId}] Removing temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, removeResult); + + var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, new Dictionary(modPaths), string.Empty, 0); + logger.LogTrace("[{ApplicationId}] Setting temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, addResult); + }).ConfigureAwait(false); + } + + public async Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData) + { + if (!IsAvailable || collectionId == Guid.Empty) + { + return; + } + + await DalamudUtil.RunOnFrameworkThread(() => + { + logger.LogTrace("[{ApplicationId}] Manip: {Data}", applicationId, manipulationData); + var result = _addTemporaryMod.Invoke("LightlessChara_Meta", collectionId, [], manipulationData, 0); + logger.LogTrace("[{ApplicationId}] Setting temp meta mod for {CollectionId}, Success: {Result}", applicationId, collectionId, result); + }).ConfigureAwait(false); + } + + protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current) + { + if (current == IpcConnectionState.Available) + { + ScheduleCleanup(); + } + else if (previous == IpcConnectionState.Available && current != IpcConnectionState.Available) + { + Interlocked.Exchange(ref _cleanupScheduled, 0); + } + } + + private void ScheduleCleanup() + { + if (Interlocked.Exchange(ref _cleanupScheduled, 1) != 0) + { + return; + } + + _ = Task.Run(CleanupTemporaryCollectionsAsync); + } + + private async Task CleanupTemporaryCollectionsAsync() + { + if (!IsAvailable) + { + return; + } + + try + { + var collections = await DalamudUtil.RunOnFrameworkThread(() => _getCollections.Invoke()).ConfigureAwait(false); + foreach (var (collectionId, name) in collections) + { + if (!IsLightlessCollectionName(name) || _activeTemporaryCollections.ContainsKey(collectionId)) + { + continue; + } + + Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId); + var deleteResult = await DalamudUtil.RunOnFrameworkThread(() => + { + var result = (PenumbraApiEc)_removeTemporaryCollection.Invoke(collectionId); + Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result); + return result; + }).ConfigureAwait(false); + + if (deleteResult == PenumbraApiEc.Success) + { + _activeTemporaryCollections.TryRemove(collectionId, out _); + } + else + { + Logger.LogDebug("Skipped removing temporary collection {CollectionName} ({CollectionId}). Result: {Result}", name, collectionId, deleteResult); + } + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to clean up Penumbra temporary collections"); + } + } + + private static bool IsLightlessCollectionName(string? name) + => !string.IsNullOrEmpty(name) && name.StartsWith("Lightless_", StringComparison.Ordinal); +} diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs new file mode 100644 index 0000000..7b3abd1 --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraRedraw.cs @@ -0,0 +1,81 @@ +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.PlayerData.Handlers; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Enums; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public sealed class PenumbraRedraw : PenumbraBase +{ + private readonly RedrawManager _redrawManager; + private readonly RedrawObject _penumbraRedraw; + private readonly EventSubscriber _penumbraObjectIsRedrawn; + + public PenumbraRedraw( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator, + RedrawManager redrawManager) : base(logger, pluginInterface, dalamudUtil, mediator) + { + _redrawManager = redrawManager; + + _penumbraRedraw = new RedrawObject(pluginInterface); + _penumbraObjectIsRedrawn = GameObjectRedrawn.Subscriber(pluginInterface, HandlePenumbraRedrawEvent); + } + + public override string Name => "Penumbra.Redraw"; + + public void CancelPendingRedraws() + => _redrawManager.Cancel(); + + public void RequestImmediateRedraw(int objectIndex, RedrawType redrawType) + { + if (!IsAvailable) + { + return; + } + + _penumbraRedraw.Invoke(objectIndex, redrawType); + } + + public async Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token) + { + if (!IsAvailable || DalamudUtil.IsZoning) + { + return; + } + + try + { + await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false); + await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, chara => + { + logger.LogDebug("[{ApplicationId}] Calling on IPC: PenumbraRedraw", applicationId); + _penumbraRedraw.Invoke(chara.ObjectIndex, RedrawType.Redraw); + }, token).ConfigureAwait(false); + } + finally + { + _redrawManager.RedrawSemaphore.Release(); + } + } + + private void HandlePenumbraRedrawEvent(IntPtr objectAddress, int objectTableIndex) + => Mediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, false)); + + protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current) + { + } + + public override void Dispose() + { + base.Dispose(); + _penumbraObjectIsRedrawn.Dispose(); + } +} diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs new file mode 100644 index 0000000..75d1d86 --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraResource.cs @@ -0,0 +1,141 @@ +using System.Collections.Concurrent; +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.PlayerData.Handlers; +using LightlessSync.Services; +using LightlessSync.Services.ActorTracking; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Helpers; +using Penumbra.Api.IpcSubscribers; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public sealed class PenumbraResource : PenumbraBase +{ + private readonly ActorObjectService _actorObjectService; + private readonly GetGameObjectResourcePaths _gameObjectResourcePaths; + private readonly ResolvePlayerPathsAsync _resolvePlayerPaths; + private readonly GetPlayerMetaManipulations _getPlayerMetaManipulations; + private readonly EventSubscriber _gameObjectResourcePathResolved; + private readonly ConcurrentDictionary _trackedActors = new(); + + public PenumbraResource( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator, + ActorObjectService actorObjectService) : base(logger, pluginInterface, dalamudUtil, mediator) + { + _actorObjectService = actorObjectService; + _gameObjectResourcePaths = new GetGameObjectResourcePaths(pluginInterface); + _resolvePlayerPaths = new ResolvePlayerPathsAsync(pluginInterface); + _getPlayerMetaManipulations = new GetPlayerMetaManipulations(pluginInterface); + _gameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pluginInterface, HandleResourceLoaded); + + foreach (var descriptor in _actorObjectService.PlayerDescriptors) + { + TrackActor(descriptor.Address); + } + } + + public override string Name => "Penumbra.Resources"; + + public async Task>?> GetCharacterDataAsync(ILogger logger, GameObjectHandler handler) + { + if (!IsAvailable) + { + return null; + } + + return await DalamudUtil.RunOnFrameworkThread(() => + { + logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths"); + var idx = handler.GetGameObject()?.ObjectIndex; + if (idx == null) + { + return null; + } + + return _gameObjectResourcePaths.Invoke(idx.Value)[0]; + }).ConfigureAwait(false); + } + + public string GetMetaManipulations() + => IsAvailable ? _getPlayerMetaManipulations.Invoke() : string.Empty; + + public async Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forwardPaths, string[] reversePaths) + { + if (!IsAvailable) + { + return (Array.Empty(), Array.Empty()); + } + + return await _resolvePlayerPaths.Invoke(forwardPaths, reversePaths).ConfigureAwait(false); + } + + public void TrackActor(nint address) + { + if (address != nint.Zero) + { + _trackedActors[(IntPtr)address] = 0; + } + } + + public void UntrackActor(nint address) + { + if (address != nint.Zero) + { + _trackedActors.TryRemove((IntPtr)address, out _); + } + } + + private void HandleResourceLoaded(nint ptr, string resolvedPath, string gamePath) + { + if (ptr == nint.Zero) + { + return; + } + + if (!_trackedActors.ContainsKey(ptr)) + { + var descriptor = _actorObjectService.PlayerDescriptors.FirstOrDefault(d => d.Address == ptr); + if (descriptor.Address != nint.Zero) + { + _trackedActors[ptr] = 0; + } + else + { + return; + } + } + + if (string.Compare(resolvedPath, gamePath, StringComparison.OrdinalIgnoreCase) == 0) + { + return; + } + + Mediator.Publish(new PenumbraResourceLoadMessage(ptr, resolvedPath, gamePath)); + } + + protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current) + { + if (current != IpcConnectionState.Available) + { + _trackedActors.Clear(); + } + else + { + foreach (var descriptor in _actorObjectService.PlayerDescriptors) + { + TrackActor(descriptor.Address); + } + } + } + + public override void Dispose() + { + base.Dispose(); + _gameObjectResourcePathResolved.Dispose(); + } +} diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs new file mode 100644 index 0000000..e12fd7b --- /dev/null +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs @@ -0,0 +1,121 @@ +using Dalamud.Plugin; +using LightlessSync.Interop.Ipc.Framework; +using LightlessSync.Services; +using LightlessSync.Services.Mediator; +using Microsoft.Extensions.Logging; +using Penumbra.Api.Enums; +using Penumbra.Api.IpcSubscribers; + +namespace LightlessSync.Interop.Ipc.Penumbra; + +public sealed class PenumbraTexture : PenumbraBase +{ + private readonly PenumbraRedraw _redrawFeature; + private readonly ConvertTextureFile _convertTextureFile; + + public PenumbraTexture( + ILogger logger, + IDalamudPluginInterface pluginInterface, + DalamudUtilService dalamudUtil, + LightlessMediator mediator, + PenumbraRedraw redrawFeature) : base(logger, pluginInterface, dalamudUtil, mediator) + { + _redrawFeature = redrawFeature; + _convertTextureFile = new ConvertTextureFile(pluginInterface); + } + + public override string Name => "Penumbra.Textures"; + + public async Task ConvertTextureFilesAsync(ILogger logger, IReadOnlyList jobs, IProgress? progress, CancellationToken token) + { + if (!IsAvailable || jobs.Count == 0) + { + return; + } + + Mediator.Publish(new HaltScanMessage(nameof(ConvertTextureFilesAsync))); + + var totalJobs = jobs.Count; + var completedJobs = 0; + + try + { + foreach (var job in jobs) + { + if (token.IsCancellationRequested) + { + break; + } + + progress?.Report(new TextureConversionProgress(completedJobs, totalJobs, job)); + await ConvertSingleJobAsync(logger, job, token).ConfigureAwait(false); + completedJobs++; + } + } + finally + { + Mediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFilesAsync))); + } + + if (completedJobs > 0 && !token.IsCancellationRequested) + { + await DalamudUtil.RunOnFrameworkThread(async () => + { + var player = await DalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false); + if (player == null) + { + return; + } + + var gameObject = await DalamudUtil.CreateGameObjectAsync(player).ConfigureAwait(false); + if (gameObject == null) + { + return; + } + + _redrawFeature.RequestImmediateRedraw(gameObject.ObjectIndex, RedrawType.Redraw); + }).ConfigureAwait(false); + } + } + + public async Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token) + { + if (!IsAvailable) + { + return; + } + + await ConvertSingleJobAsync(Logger, job, token).ConfigureAwait(false); + } + + private async Task ConvertSingleJobAsync(ILogger logger, TextureConversionJob job, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + + logger.LogInformation("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType); + var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps); + await convertTask.ConfigureAwait(false); + + if (!convertTask.IsCompletedSuccessfully || job.DuplicateTargets is not { Count: > 0 }) + { + return; + } + + foreach (var duplicate in job.DuplicateTargets) + { + try + { + logger.LogInformation("Synchronizing duplicate {Duplicate}", duplicate); + File.Copy(job.OutputFile, duplicate, overwrite: true); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to copy duplicate {Duplicate}", duplicate); + } + } + } + + protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current) + { + } +} diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 1842989..5136a6e 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -228,7 +228,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton((s) => new IpcCallerPetNames(s.GetRequiredService>(), pluginInterface, s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton((s) => new IpcCallerBrio(s.GetRequiredService>(), pluginInterface, - s.GetRequiredService())); + s.GetRequiredService(), s.GetRequiredService())); collection.AddSingleton((s) => new IpcManager(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(),