using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using Dalamud.Game.ClientState.Objects.Types; using MareSynchronos.Utils; using Action = System.Action; using System.Collections.Concurrent; using System.Text; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; using MareSynchronos.Mediator; namespace MareSynchronos.Managers; public class IpcManager : MediatorSubscriberBase, IDisposable { private readonly ICallGateSubscriber _glamourerApiVersion; private readonly ICallGateSubscriber? _glamourerApplyAll; private readonly ICallGateSubscriber? _glamourerGetAllCustomization; private readonly ICallGateSubscriber _glamourerRevertCustomization; private readonly ICallGateSubscriber? _glamourerApplyOnlyEquipment; private readonly ICallGateSubscriber? _glamourerApplyOnlyCustomization; private readonly FuncSubscriber<(int, int)> _penumbraApiVersion; private readonly FuncSubscriber _penumbraCreateNamedTemporaryCollection; private readonly FuncSubscriber _penumbraGetMetaManipulations; private readonly EventSubscriber _penumbraInit; private readonly EventSubscriber _penumbraDispose; private readonly EventSubscriber _penumbraObjectIsRedrawn; private readonly ActionSubscriber _penumbraRedraw; private readonly ActionSubscriber _penumbraRedrawObject; private readonly FuncSubscriber _penumbraRemoveTemporaryCollection; private readonly FuncSubscriber _penumbraRemoveTemporaryMod; private readonly FuncSubscriber _penumbraAssignTemporaryCollection; private readonly FuncSubscriber _penumbraResolveModDir; private readonly FuncSubscriber _penumbraResolvePlayer; private readonly FuncSubscriber _reverseResolvePlayer; private readonly FuncSubscriber, string, int, PenumbraApiEc> _penumbraAddTemporaryMod; private readonly FuncSubscriber _penumbraResolvePaths; private readonly FuncSubscriber _penumbraEnabled; private readonly EventSubscriber _penumbraGameObjectResourcePathResolved; private readonly ICallGateSubscriber _heelsGetApiVersion; private readonly ICallGateSubscriber _heelsGetOffset; private readonly ICallGateSubscriber _heelsOffsetUpdate; private readonly ICallGateSubscriber _heelsRegisterPlayer; private readonly ICallGateSubscriber _heelsUnregisterPlayer; private readonly ICallGateSubscriber _customizePlusApiVersion; private readonly ICallGateSubscriber _customizePlusGetBodyScale; private readonly ICallGateSubscriber _customizePlusSetBodyScaleToCharacter; private readonly ICallGateSubscriber _customizePlusRevert; private readonly ICallGateSubscriber _customizePlusOnScaleUpdate; private readonly ICallGateSubscriber _palettePlusApiVersion; private readonly ICallGateSubscriber _palettePlusBuildCharaPalette; private readonly ICallGateSubscriber _palettePlusSetCharaPalette; private readonly ICallGateSubscriber _palettePlusRemoveCharaPalette; private readonly ICallGateSubscriber _palettePlusPaletteChanged; private readonly DalamudUtil _dalamudUtil; private bool _inGposeQueueMode = false; private ConcurrentQueue ActionQueue => _inGposeQueueMode ? _gposeActionQueue : _normalQueue; private readonly ConcurrentQueue _normalQueue = new(); private readonly ConcurrentQueue _gposeActionQueue = new(); public IpcManager(DalamudPluginInterface pi, DalamudUtil dalamudUtil, MareMediator mediator) : base(mediator) { Logger.Verbose("Creating " + nameof(IpcManager)); _penumbraInit = Penumbra.Api.Ipc.Initialized.Subscriber(pi, () => PenumbraInit()); _penumbraDispose = Penumbra.Api.Ipc.Disposed.Subscriber(pi, () => PenumbraDispose()); _penumbraResolvePlayer = Penumbra.Api.Ipc.ResolvePlayerPath.Subscriber(pi); _penumbraResolveModDir = Penumbra.Api.Ipc.GetModDirectory.Subscriber(pi); _penumbraRedraw = Penumbra.Api.Ipc.RedrawObjectByName.Subscriber(pi); _penumbraRedrawObject = Penumbra.Api.Ipc.RedrawObject.Subscriber(pi); _reverseResolvePlayer = Penumbra.Api.Ipc.ReverseResolvePlayerPath.Subscriber(pi); _penumbraApiVersion = Penumbra.Api.Ipc.ApiVersions.Subscriber(pi); _penumbraObjectIsRedrawn = Penumbra.Api.Ipc.GameObjectRedrawn.Subscriber(pi, (ptr, idx) => RedrawEvent((IntPtr)ptr, idx)); _penumbraGetMetaManipulations = Penumbra.Api.Ipc.GetPlayerMetaManipulations.Subscriber(pi); _penumbraAddTemporaryMod = Penumbra.Api.Ipc.AddTemporaryMod.Subscriber(pi); _penumbraCreateNamedTemporaryCollection = Penumbra.Api.Ipc.CreateNamedTemporaryCollection.Subscriber(pi); _penumbraRemoveTemporaryCollection = Penumbra.Api.Ipc.RemoveTemporaryCollectionByName.Subscriber(pi); _penumbraRemoveTemporaryMod = Penumbra.Api.Ipc.RemoveTemporaryMod.Subscriber(pi); _penumbraAssignTemporaryCollection = Penumbra.Api.Ipc.AssignTemporaryCollection.Subscriber(pi); _penumbraResolvePaths = Penumbra.Api.Ipc.ResolvePlayerPaths.Subscriber(pi); _penumbraEnabled = Penumbra.Api.Ipc.GetEnabledState.Subscriber(pi); _penumbraGameObjectResourcePathResolved = Penumbra.Api.Ipc.GameObjectResourcePathResolved.Subscriber(pi, (ptr, arg1, arg2) => ResourceLoaded((IntPtr)ptr, arg1, arg2)); _glamourerApiVersion = pi.GetIpcSubscriber("Glamourer.ApiVersion"); _glamourerGetAllCustomization = pi.GetIpcSubscriber("Glamourer.GetAllCustomizationFromCharacter"); _glamourerApplyAll = pi.GetIpcSubscriber("Glamourer.ApplyAllToCharacter"); _glamourerApplyOnlyCustomization = pi.GetIpcSubscriber("Glamourer.ApplyOnlyCustomizationToCharacter"); _glamourerApplyOnlyEquipment = pi.GetIpcSubscriber("Glamourer.ApplyOnlyEquipmentToCharacter"); _glamourerRevertCustomization = pi.GetIpcSubscriber("Glamourer.RevertCharacter"); _heelsGetApiVersion = pi.GetIpcSubscriber("HeelsPlugin.ApiVersion"); _heelsGetOffset = pi.GetIpcSubscriber("HeelsPlugin.GetOffset"); _heelsRegisterPlayer = pi.GetIpcSubscriber("HeelsPlugin.RegisterPlayer"); _heelsUnregisterPlayer = pi.GetIpcSubscriber("HeelsPlugin.UnregisterPlayer"); _heelsOffsetUpdate = pi.GetIpcSubscriber("HeelsPlugin.OffsetChanged"); _heelsOffsetUpdate.Subscribe(HeelsOffsetChange); _customizePlusApiVersion = pi.GetIpcSubscriber("CustomizePlus.GetApiVersion"); _customizePlusGetBodyScale = pi.GetIpcSubscriber("CustomizePlus.GetBodyScale"); _customizePlusRevert = pi.GetIpcSubscriber("CustomizePlus.RevertCharacter"); _customizePlusSetBodyScaleToCharacter = pi.GetIpcSubscriber("CustomizePlus.SetBodyScaleToCharacter"); _customizePlusOnScaleUpdate = pi.GetIpcSubscriber("CustomizePlus.OnScaleUpdate"); _customizePlusOnScaleUpdate.Subscribe(OnCustomizePlusScaleChange); _palettePlusApiVersion = pi.GetIpcSubscriber("PalettePlus.ApiVersion"); _palettePlusBuildCharaPalette = pi.GetIpcSubscriber("PalettePlus.BuildCharaPalette"); _palettePlusSetCharaPalette = pi.GetIpcSubscriber("PalettePlus.SetCharaPalette"); _palettePlusRemoveCharaPalette = pi.GetIpcSubscriber("PalettePlus.RemoveCharaPalette"); _palettePlusPaletteChanged = pi.GetIpcSubscriber("PalettePlus.PaletteChanged"); _palettePlusPaletteChanged.Subscribe(OnPalettePlusPaletteChange); if (Initialized) { Mediator.Publish(new PenumbraInitializedMessage()); } _dalamudUtil = dalamudUtil; Mediator.Subscribe(this, (_) => HandleActionQueue()); Mediator.Subscribe(this, (_) => HandleGposeActionQueue()); Mediator.Subscribe(this, (_) => ClearActionQueue()); Mediator.Subscribe(this, (_) => CheckPenumbraModPath()); } private void CheckPenumbraModPath() { PenumbraModDirectory = GetPenumbraModDirectory(); } private void HandleGposeActionQueue() { if (_gposeActionQueue.TryDequeue(out var action)) { if (action == null) return; Logger.Debug("Execution action in gpose queue: " + action.Method); action(); } } public void ToggleGposeQueueMode(bool on) { _inGposeQueueMode = on; } private void ClearActionQueue() { ActionQueue.Clear(); _gposeActionQueue.Clear(); } private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) { Task.Run(() => { if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, ignoreCase: true, System.Globalization.CultureInfo.InvariantCulture) != 0) { Mediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2)); } }); } private void HandleActionQueue() { if (ActionQueue.TryDequeue(out var action)) { if (action == null) return; Logger.Debug("Execution action in queue: " + action.Method); action(); } } public bool Initialized => CheckPenumbraApi() && CheckGlamourerApi(); public bool CheckGlamourerApi() { try { return _glamourerApiVersion.InvokeFunc() >= 0; } catch { return false; } } public bool CheckPenumbraApi() { try { return _penumbraApiVersion.Invoke() is { Item1: 4, Item2: >= 19 } && _penumbraEnabled.Invoke(); } catch { return false; } } public bool CheckHeelsApi() { try { return string.Equals(_heelsGetApiVersion.InvokeFunc(), "1.0.1", StringComparison.Ordinal); } catch { return false; } } public bool CheckCustomizePlusApi() { try { return string.Equals(_customizePlusApiVersion.InvokeFunc(), "1.0", StringComparison.Ordinal); } catch { return false; } } public bool CheckPalettePlusApi() { try { return string.Equals(_palettePlusApiVersion.InvokeFunc(), "1.0.0", StringComparison.Ordinal); } catch { return false; } } public override void Dispose() { base.Dispose(); int totalSleepTime = 0; while (!ActionQueue.IsEmpty && totalSleepTime < 2000) { Logger.Verbose("Waiting for actionqueue to clear..."); HandleActionQueue(); Thread.Sleep(16); totalSleepTime += 16; } if (totalSleepTime >= 2000) { Logger.Verbose("Action queue clear or not, disposing"); } ActionQueue.Clear(); _penumbraGameObjectResourcePathResolved.Dispose(); _penumbraDispose.Dispose(); _penumbraInit.Dispose(); _penumbraObjectIsRedrawn.Dispose(); _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); } public float GetHeelsOffset() { if (!CheckHeelsApi()) return 0.0f; return _heelsGetOffset.InvokeFunc(); } public void HeelsSetOffsetForPlayer(float offset, IntPtr character) { if (!CheckHeelsApi()) return; ActionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(character); if (gameObj != null) { Logger.Verbose("Applying Heels data to " + character.ToString("X")); _heelsRegisterPlayer.InvokeAction(gameObj, offset); } }); } public void HeelsRestoreOffsetForPlayer(IntPtr character) { if (!CheckHeelsApi()) return; ActionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(character); if (gameObj != null) { Logger.Verbose("Restoring Heels data to " + character.ToString("X")); _heelsUnregisterPlayer.InvokeAction(gameObj); } }); } public string GetCustomizePlusScale() { if (!CheckCustomizePlusApi()) return string.Empty; var scale = _customizePlusGetBodyScale.InvokeFunc(_dalamudUtil.PlayerName); if (string.IsNullOrEmpty(scale)) return string.Empty; return Convert.ToBase64String(Encoding.UTF8.GetBytes(scale)); } public void CustomizePlusSetBodyScale(IntPtr character, string scale) { if (!CheckCustomizePlusApi() || string.IsNullOrEmpty(scale)) return; ActionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(character); if (gameObj is Character c) { string decodedScale = Encoding.UTF8.GetString(Convert.FromBase64String(scale)); Logger.Verbose("CustomizePlus applying for " + c.Address.ToString("X")); _customizePlusSetBodyScaleToCharacter!.InvokeAction(decodedScale, c); } }); } public void CustomizePlusRevert(IntPtr character) { if (!CheckCustomizePlusApi()) return; ActionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(character); if (gameObj is Character c) { Logger.Verbose("CustomizePlus reverting for " + c.Address.ToString("X")); _customizePlusRevert!.InvokeAction(c); } }); } public void GlamourerApplyAll(string? customization, IntPtr obj) { if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; ActionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(obj); if (gameObj is Character c) { Logger.Verbose("Glamourer applying for " + c.Address.ToString("X")); _glamourerApplyAll!.InvokeAction(customization, c); } }); } public void GlamourerApplyOnlyEquipment(string customization, IntPtr character) { if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; ActionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(character); if (gameObj is Character c) { Logger.Verbose("Glamourer apply only equipment to " + c.Address.ToString("X")); _glamourerApplyOnlyEquipment!.InvokeAction(customization, c); } }); } public void GlamourerApplyOnlyCustomization(string customization, IntPtr character) { if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; ActionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(character); if (gameObj is Character c) { Logger.Verbose("Glamourer apply only customization to " + c.Address.ToString("X")); _glamourerApplyOnlyCustomization!.InvokeAction(customization, c); } }); } public string GlamourerGetCharacterCustomization(IntPtr character) { if (!CheckGlamourerApi()) return string.Empty; try { var gameObj = _dalamudUtil.CreateGameObject(character); if (gameObj is Character c) { var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(c); byte[] bytes = Convert.FromBase64String(glamourerString); // ignore transparency bytes[88] = 128; bytes[89] = 63; return Convert.ToBase64String(bytes); } return string.Empty; } catch { return string.Empty; } } public void GlamourerRevertCharacterCustomization(GameObject character) { if (!CheckGlamourerApi()) return; ActionQueue.Enqueue(() => _glamourerRevertCustomization!.InvokeAction(character)); } public string PenumbraGetMetaManipulations() { if (!CheckPenumbraApi()) return string.Empty; return _penumbraGetMetaManipulations.Invoke(); } public string? PenumbraModDirectory; public string? GetPenumbraModDirectory() { if (!CheckPenumbraApi()) return null; return _penumbraResolveModDir!.Invoke().ToLowerInvariant(); } public void PenumbraRedraw(IntPtr obj) { if (!CheckPenumbraApi()) return; ActionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(obj); if (gameObj != null) { Logger.Verbose("Redrawing " + gameObj); _penumbraRedrawObject!.Invoke(gameObj, RedrawType.Redraw); } }); } public void PenumbraRedraw(string actorName) { if (!CheckPenumbraApi()) return; ActionQueue.Enqueue(() => _penumbraRedraw!.Invoke(actorName, RedrawType.Redraw)); } public void PenumbraRemoveTemporaryCollection(string characterName) { if (!CheckPenumbraApi()) return; ActionQueue.Enqueue(() => { var collName = "Mare_" + characterName; Logger.Verbose("Removing temp collection for " + collName); var ret = _penumbraRemoveTemporaryMod.Invoke("MaraChara", collName, 0); Logger.Verbose("RemoveTemporaryMod: " + ret); var ret2 = _penumbraRemoveTemporaryCollection.Invoke(collName); Logger.Verbose("RemoveTemporaryCollection: " + ret2); }); } public string PenumbraResolvePath(string path) { if (!CheckPenumbraApi()) return path; var resolvedPath = _penumbraResolvePlayer!.Invoke(path); return resolvedPath ?? path; } public string[] PenumbraReverseResolvePlayer(string path) { if (!CheckPenumbraApi()) return new[] { path }; var resolvedPaths = _reverseResolvePlayer.Invoke(path); if (resolvedPaths.Length == 0) { resolvedPaths = new[] { path }; } return resolvedPaths; } public void PenumbraSetTemporaryMods(string characterName, Dictionary modPaths, string manipulationData) { if (!CheckPenumbraApi()) return; ActionQueue.Enqueue(() => { var idx = _dalamudUtil.GetIndexFromObjectTableByName(characterName); if (idx == null) { return; } var collName = "Mare_" + characterName; var ret = _penumbraCreateNamedTemporaryCollection.Invoke(collName); Logger.Verbose("Creating Temp Collection " + collName + ", Success: " + ret); var retAssign = _penumbraAssignTemporaryCollection.Invoke(collName, idx.Value, c: true); Logger.Verbose("Assigning Temp Collection " + collName + " to index " + idx.Value); foreach (var mod in modPaths) { Logger.Verbose(mod.Key + " => " + mod.Value); } var ret2 = _penumbraAddTemporaryMod.Invoke("MareChara", collName, modPaths, manipulationData, 0); Logger.Verbose("Setting temp mods for " + collName + ", Success: " + ret2); }); } public (string[] forward, string[][] reverse) PenumbraResolvePaths(string[] forward, string[] reverse) { return _penumbraResolvePaths.Invoke(forward, reverse); } private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) { Mediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex)); } private void PenumbraInit() { Mediator.Publish(new PenumbraInitializedMessage()); _penumbraRedraw!.Invoke("self", RedrawType.Redraw); } private void HeelsOffsetChange(float offset) { Mediator.Publish(new HeelsOffsetMessage(offset)); } private void OnCustomizePlusScaleChange(string? scale) { if (scale != null) scale = Convert.ToBase64String(Encoding.UTF8.GetBytes(scale)); Mediator.Publish(new CustomizePlusMessage(scale)); } private void OnPalettePlusPaletteChange(Character character, string palette) { if (character.Address == 0 || character.Address != _dalamudUtil.PlayerPointer) return; if (palette != null) palette = Convert.ToBase64String(Encoding.UTF8.GetBytes(palette)); Mediator.Publish(new PalettePlusMessage(palette)); } public void PalettePlusSetPalette(IntPtr character, string palette) { if (!CheckPalettePlusApi()) return; ActionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(character); if (gameObj is Character c) { string decodedPalette = Encoding.UTF8.GetString(Convert.FromBase64String(palette)); if (string.IsNullOrEmpty(decodedPalette)) { Logger.Verbose("PalettePlus removing for " + c.Address.ToString("X")); _palettePlusRemoveCharaPalette!.InvokeAction(c); } else { Logger.Verbose("PalettePlus applying for " + c.Address.ToString("X")); _palettePlusSetCharaPalette!.InvokeAction(c, decodedPalette); } } }); } public string PalettePlusBuildPalette() { if (!CheckPalettePlusApi()) return string.Empty; var palette = _palettePlusBuildCharaPalette.InvokeFunc(_dalamudUtil.PlayerCharacter); if (string.IsNullOrEmpty(palette)) return string.Empty; return Convert.ToBase64String(Encoding.UTF8.GetBytes(palette)); } public void PalettePlusRemovePalette(IntPtr character) { if (!CheckPalettePlusApi()) return; ActionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(character); if (gameObj is Character c) { Logger.Verbose("PalettePlus removing for " + c.Address.ToString("X")); _palettePlusRemoveCharaPalette!.InvokeAction(c); } }); } private void PenumbraDispose() { Mediator.Publish(new PenumbraDisposedMessage()); ActionQueue.Clear(); } }