using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using System; using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; using MareSynchronos.Utils; using MareSynchronos.WebAPI; using Action = System.Action; using System.Collections.Concurrent; namespace MareSynchronos.Managers { public delegate void PenumbraRedrawEvent(IntPtr address, int objTblIdx); public delegate void PenumbraResourceLoadEvent(IntPtr drawObject, string gamePath, string filePath); public class IpcManager : 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 ICallGateSubscriber<(int, int)> _penumbraApiVersion; private readonly ICallGateSubscriber _penumbraCreateTemporaryCollection; private readonly ICallGateSubscriber _penumbraGetMetaManipulations; private readonly ICallGateSubscriber _penumbraInit; private readonly ICallGateSubscriber _penumbraDispose; private readonly ICallGateSubscriber _penumbraObjectIsRedrawn; private readonly ICallGateSubscriber? _penumbraRedraw; private readonly ICallGateSubscriber? _penumbraRedrawObject; private readonly ICallGateSubscriber _penumbraRemoveTemporaryCollection; private readonly ICallGateSubscriber? _penumbraResolveModDir; private readonly ICallGateSubscriber? _penumbraResolvePlayer; private readonly ICallGateSubscriber? _reverseResolvePlayer; private readonly ICallGateSubscriber, string, int, int> _penumbraSetTemporaryMod; private readonly ICallGateSubscriber _penumbraGameObjectResourcePathResolved; private readonly DalamudUtil _dalamudUtil; private readonly ConcurrentQueue actionQueue = new(); public IpcManager(DalamudPluginInterface pi, DalamudUtil dalamudUtil) { Logger.Verbose("Creating " + nameof(IpcManager)); _penumbraInit = pi.GetIpcSubscriber("Penumbra.Initialized"); _penumbraDispose = pi.GetIpcSubscriber("Penumbra.Disposed"); _penumbraResolvePlayer = pi.GetIpcSubscriber("Penumbra.ResolvePlayerPath"); _penumbraResolveModDir = pi.GetIpcSubscriber("Penumbra.GetModDirectory"); _penumbraRedraw = pi.GetIpcSubscriber("Penumbra.RedrawObjectByName"); _penumbraRedrawObject = pi.GetIpcSubscriber("Penumbra.RedrawObject"); _reverseResolvePlayer = pi.GetIpcSubscriber("Penumbra.ReverseResolvePlayerPath"); _penumbraApiVersion = pi.GetIpcSubscriber<(int, int)>("Penumbra.ApiVersions"); _penumbraObjectIsRedrawn = pi.GetIpcSubscriber("Penumbra.GameObjectRedrawn"); _penumbraGetMetaManipulations = pi.GetIpcSubscriber("Penumbra.GetPlayerMetaManipulations"); _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"); _penumbraGameObjectResourcePathResolved = pi.GetIpcSubscriber("Penumbra.GameObjectResourcePathResolved"); _penumbraGameObjectResourcePathResolved.Subscribe(ResourceLoaded); _penumbraObjectIsRedrawn.Subscribe(RedrawEvent); _penumbraInit.Subscribe(PenumbraInit); _penumbraDispose.Subscribe(PenumbraDispose); _penumbraSetTemporaryMod = pi .GetIpcSubscriber, string, int, int>("Penumbra.AddTemporaryMod"); _penumbraCreateTemporaryCollection = pi.GetIpcSubscriber("Penumbra.CreateTemporaryCollection"); _penumbraRemoveTemporaryCollection = pi.GetIpcSubscriber("Penumbra.RemoveTemporaryCollection"); if (Initialized) { PenumbraInitialized?.Invoke(); } this._dalamudUtil = dalamudUtil; _dalamudUtil.FrameworkUpdate += HandleActionQueue; } private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) { if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, true, System.Globalization.CultureInfo.InvariantCulture) != 0) { PenumbraResourceLoadEvent?.Invoke(ptr, arg1, arg2); //Logger.Debug($"Resolved {ptr:X}: {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 event VoidDelegate? PenumbraInitialized; public event VoidDelegate? PenumbraDisposed; public event PenumbraRedrawEvent? PenumbraRedrawEvent; public event PenumbraResourceLoadEvent? PenumbraResourceLoadEvent; public bool Initialized => CheckPenumbraApi(); public bool CheckGlamourerApi() { try { return _glamourerApiVersion.InvokeFunc() >= 0; } catch { return false; } } public bool CheckPenumbraApi() { try { return _penumbraApiVersion.InvokeFunc() is { Item1: 4, Item2: >= 13 }; } catch { return false; } } public void Dispose() { Logger.Verbose("Disposing " + nameof(IpcManager)); int totalSleepTime = 0; while (actionQueue.Count > 0 && totalSleepTime < 2000) { System.Threading.Thread.Sleep(16); totalSleepTime += 16; } _dalamudUtil.FrameworkUpdate -= HandleActionQueue; actionQueue.Clear(); _penumbraDispose.Unsubscribe(PenumbraDispose); _penumbraInit.Unsubscribe(PenumbraInit); _penumbraObjectIsRedrawn.Unsubscribe(RedrawEvent); _penumbraGameObjectResourcePathResolved.Unsubscribe(ResourceLoaded); } public void GlamourerApplyAll(string? customization, IntPtr obj) { if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; actionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(obj); if (gameObj != null) { Logger.Verbose("Glamourer applying for " + gameObj); _glamourerApplyAll!.InvokeAction(customization, gameObj); } }); } public void GlamourerApplyOnlyEquipment(string customization, IntPtr character) { if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; actionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(character); if (gameObj != null) { Logger.Verbose("Glamourer apply only equipment to " + character.ToString("X")); _glamourerApplyOnlyEquipment!.InvokeAction(customization, gameObj); } }); } public void GlamourerApplyOnlyCustomization(string customization, IntPtr character) { if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; actionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(character); if (gameObj != null) { Logger.Verbose("Glamourer apply only customization to " + character.ToString("X")); _glamourerApplyOnlyCustomization!.InvokeAction(customization, gameObj); } }); } public string GlamourerGetCharacterCustomization(IntPtr character) { if (!CheckGlamourerApi()) return string.Empty; try { var gameObj = _dalamudUtil.CreateGameObject(character); if (gameObj != null) { var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(gameObj); 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.InvokeFunc(); } public string? PenumbraModDirectory() { if (!CheckPenumbraApi()) return null; return _penumbraResolveModDir!.InvokeFunc(); } public void PenumbraRedraw(IntPtr obj) { if (!CheckPenumbraApi()) return; actionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(obj); if (gameObj != null) { Logger.Verbose("Redrawing " + gameObj); _penumbraRedrawObject!.InvokeAction(gameObj, 0); } }); } public void PenumbraRedraw(string actorName) { if (!CheckPenumbraApi()) return; actionQueue.Enqueue(() => _penumbraRedraw!.InvokeAction(actorName, 0)); } public void PenumbraRemoveTemporaryCollection(string characterName) { if (!CheckPenumbraApi()) return; actionQueue.Enqueue(() => { Logger.Verbose("Removing temp collection for " + characterName); _penumbraRemoveTemporaryCollection.InvokeFunc(characterName); }); } public string PenumbraResolvePath(string path) { if (!CheckPenumbraApi()) return path; var resolvedPath = _penumbraResolvePlayer!.InvokeFunc(path); return resolvedPath ?? path; } public string[] PenumbraReverseResolvePlayer(string path) { if (!CheckPenumbraApi()) return new[] { path }; var resolvedPaths = _reverseResolvePlayer!.InvokeFunc(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 ret = _penumbraCreateTemporaryCollection.InvokeFunc("MareSynchronos", characterName, true); Logger.Verbose("Assigning temp mods for " + ret.Item2); foreach (var mod in modPaths) { Logger.Verbose(mod.Key + " => " + mod.Value); } _penumbraSetTemporaryMod.InvokeFunc("MareSynchronos", ret.Item2, modPaths, manipulationData, 0); }); } private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) { PenumbraRedrawEvent?.Invoke(objectAddress, objectTableIndex); } private void PenumbraInit() { PenumbraInitialized?.Invoke(); _penumbraRedraw!.InvokeAction("self", 0); } private void PenumbraDispose() { PenumbraDisposed?.Invoke(); actionQueue.Clear(); } } }