From 176eb2a344ad801203bbf8121d2733450a150617 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Fri, 17 Jun 2022 01:08:36 +0200 Subject: [PATCH] remove DrawHooks, use new penumbra IPC calls (otter pls) --- .../Factories/FileReplacementFactory.cs | 31 +- MareSynchronos/Hooks/DrawHooks.cs | 433 ------------------ MareSynchronos/Managers/CharacterManager.cs | 195 ++++++-- MareSynchronos/Managers/IpcManager.cs | 28 +- MareSynchronos/Models/CharacterCache.cs | 25 +- MareSynchronos/Models/FileReplacement.cs | 41 +- MareSynchronos/Plugin.cs | 37 +- 7 files changed, 230 insertions(+), 560 deletions(-) delete mode 100644 MareSynchronos/Hooks/DrawHooks.cs diff --git a/MareSynchronos/Factories/FileReplacementFactory.cs b/MareSynchronos/Factories/FileReplacementFactory.cs index 70a6e62..85e7e08 100644 --- a/MareSynchronos/Factories/FileReplacementFactory.cs +++ b/MareSynchronos/Factories/FileReplacementFactory.cs @@ -7,45 +7,20 @@ namespace MareSynchronos.Factories public class FileReplacementFactory { private readonly IpcManager ipcManager; - private readonly ClientState clientState; - private string playerName; - public FileReplacementFactory(IpcManager ipcManager, ClientState clientState) + public FileReplacementFactory(IpcManager ipcManager) { this.ipcManager = ipcManager; - this.clientState = clientState; - playerName = null!; } - public FileReplacement Create(string gamePath, bool resolve = true) + public FileReplacement Create() { if (!ipcManager.CheckPenumbraAPI()) { throw new System.Exception(); } - var fileReplacement = new FileReplacement(gamePath, ipcManager.PenumbraModDirectory()!); - if (!resolve) return fileReplacement; - - if (clientState.LocalPlayer != null) - { - playerName = clientState.LocalPlayer.Name.ToString(); - } - fileReplacement.SetResolvedPath(ipcManager.PenumbraResolvePath(gamePath, playerName)!); - if (!fileReplacement.HasFileReplacement) - { - // try to resolve path with --filename instead? - string[] tempGamePath = gamePath.Split('/'); - tempGamePath[^1] = "--" + tempGamePath[^1]; - string newTempGamePath = string.Join('/', tempGamePath); - var resolvedPath = ipcManager.PenumbraResolvePath(newTempGamePath, playerName)!; - if (resolvedPath != newTempGamePath) - { - fileReplacement.SetResolvedPath(resolvedPath); - fileReplacement.SetGamePath(newTempGamePath); - } - } - return fileReplacement; + return new FileReplacement(ipcManager.PenumbraModDirectory()!); } } } diff --git a/MareSynchronos/Hooks/DrawHooks.cs b/MareSynchronos/Hooks/DrawHooks.cs deleted file mode 100644 index 8545165..0000000 --- a/MareSynchronos/Hooks/DrawHooks.cs +++ /dev/null @@ -1,433 +0,0 @@ -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.Gui; -using Dalamud.Hooking; -using Dalamud.Logging; -using Dalamud.Plugin; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; -using FFXIVClientStructs.FFXIV.Client.System.Resource; -using FFXIVClientStructs.FFXIV.Client.UI; -using FFXIVClientStructs.FFXIV.Component.GUI; -using MareSynchronos.Factories; -using MareSynchronos.Managers; -using MareSynchronos.Models; -using Penumbra.GameData.ByteString; -using Penumbra.Interop.Structs; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -namespace MareSynchronos.Hooks -{ - public unsafe class DrawHooks : IDisposable - { - public const int ResolveMdlIdx = 73; - public const int ResolveMtrlIdx = 82; - - [Signature("E8 ?? ?? ?? ?? 48 85 C0 74 21 C7 40", DetourName = "CharacterBaseCreateDetour")] - public Hook? CharacterBaseCreateHook; - [Signature("E8 ?? ?? ?? ?? 40 F6 C7 01 74 3A 40 F6 C7 04 75 27 48 85 DB 74 2F 48 8B 05 ?? ?? ?? ?? 48 8B D3 48 8B 48 30", - DetourName = "CharacterBaseDestructorDetour")] - public Hook? CharacterBaseDestructorHook; - [Signature("48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1", ScanType = ScanType.StaticAddress)] - public IntPtr* DrawObjectHumanVTable; - [Signature("E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? 33 D2 E8 ?? ?? ?? ?? 84 C0")] - public Hook? EnableDrawHook; - [Signature("4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = "LoadMtrlTexDetour")] - public Hook? LoadMtrlTexHook; - - public event EventHandler? PlayerLoadEvent; - - public Hook? ResolveMdlPathHook; - public Hook? ResolveMtrlPathHook; - private readonly ClientState clientState; - private readonly Dictionary DrawObjectToObject = new(); - private readonly FileReplacementFactory factory; - private readonly GameGui gameGui; - private readonly ObjectTable objectTable; - private readonly DalamudPluginInterface pluginInterface; - private ConcurrentDictionary cachedResources = new(); - private GameObject* lastGameObject = null; - private ConcurrentBag loadedMaterials = new(); - - public DrawHooks(DalamudPluginInterface pluginInterface, ClientState clientState, ObjectTable objectTable, FileReplacementFactory factory, GameGui gameGui) - { - this.pluginInterface = pluginInterface; - this.clientState = clientState; - this.objectTable = objectTable; - this.factory = factory; - this.gameGui = gameGui; - SignatureHelper.Initialise(this); - } - - public delegate IntPtr CharacterBaseCreateDelegate(uint a, IntPtr b, IntPtr c, byte d); - public delegate void CharacterBaseDestructorDelegate(IntPtr drawBase); - public delegate void EnableDrawDelegate(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d); - public delegate IntPtr GeneralResolveDelegate(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4); - public delegate byte LoadMtrlFilesDelegate(IntPtr mtrlResourceHandle); - public delegate IntPtr MaterialResolveDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5); - public delegate void OnModelLoadCompleteDelegate(IntPtr drawObject); - public void Dispose() - { - DisableHumanHooks(); - DisposeHumanHooks(); - } - - public CharacterCache BuildCharacterCache() - { - foreach (var resource in cachedResources) - { - resource.Value.IsInUse = false; - resource.Value.ImcData = string.Empty; - resource.Value.Associated.Clear(); - } - - PluginLog.Verbose("Invaldated resource cache"); - - var cache = new CharacterCache(); - - try - { - var model = (CharacterBase*)((Character*)clientState.LocalPlayer!.Address)->GameObject.GetDrawObject(); - for (var idx = 0; idx < model->SlotCount; ++idx) - { - var mdl = (RenderModel*)model->ModelArray[idx]; - if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) - { - continue; - } - - var mdlResource = factory.Create(new Utf8String(mdl->ResourceHandle->FileName()).ToString()); - var cachedMdlResource = cachedResources.First(r => r.Value.IsReplacedByThis(mdlResource)).Value; - - var imc = (ResourceHandle*)model->IMCArray[idx]; - if (imc != null) - { - byte[] imcData = new byte[imc->Data->DataLength / sizeof(long)]; - Marshal.Copy((IntPtr)imc->Data->DataPtr, imcData, 0, (int)imc->Data->DataLength / sizeof(long)); - string imcDataStr = BitConverter.ToString(imcData).Replace("-", ""); - cachedMdlResource.ImcData = imcDataStr; - } - cache.AddAssociatedResource(cachedMdlResource, null!, null!); - - for (int mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) - { - var mtrl = (Material*)mdl->Materials[mtrlIdx]; - if (mtrl == null) continue; - - var mtrlFileResource = factory.Create(new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2]); - var cachedMtrlResource = cachedResources.First(r => r.Value.IsReplacedByThis(mtrlFileResource)).Value; - cache.AddAssociatedResource(cachedMtrlResource, cachedMdlResource, null!); - - var mtrlResource = (MtrlResource*)mtrl->ResourceHandle; - for (int resIdx = 0; resIdx < mtrlResource->NumTex; resIdx++) - { - var texPath = new Utf8String(mtrlResource->TexString(resIdx)); - - if (string.IsNullOrEmpty(texPath.ToString())) continue; - - var texResource = factory.Create(texPath.ToString()); - var cachedTexResource = cachedResources.First(r => r.Value.IsReplacedByThis(texResource)).Value; - cache.AddAssociatedResource(cachedTexResource, cachedMdlResource, cachedMtrlResource); - } - } - } - } - catch (Exception ex) - { - PluginLog.Error(ex, ex.Message); - } - - return cache; - } - - public void PrintRequestedResources() - { - var cache = BuildCharacterCache(); - - PluginLog.Verbose("--- CURRENTLY LOADED FILES ---"); - - PluginLog.Verbose(cache.ToString()); - - PluginLog.Verbose("--- LOOSE FILES ---"); - - foreach (var resource in cachedResources.Where(r => !r.Value.IsInUse).OrderBy(a => a.Value.GamePath)) - { - PluginLog.Verbose(resource.Value.ToString()); - } - } - - public void StartHooks() - { - cachedResources.Clear(); - SetupHumanHooks(); - EnableHumanHooks(); - PluginLog.Verbose("Hooks enabled"); - } - - public void StopHooks() - { - DisableHumanHooks(); - DisposeHumanHooks(); - } - - private void AddRequestedResource(FileReplacement replacement) - { - if (!cachedResources.Any(a => a.Value.IsReplacedByThis(replacement)) || cachedResources.Any(c => c.Key == replacement.GamePath)) - { - cachedResources[replacement.GamePath] = replacement; - } - } - - private IntPtr CharacterBaseCreateDetour(uint a, IntPtr b, IntPtr c, byte d) - { - var ret = CharacterBaseCreateHook!.Original(a, b, c, d); - if (lastGameObject != null) - { - DrawObjectToObject[ret] = (lastGameObject->ObjectIndex); - } - - return ret; - } - - private void CharacterBaseDestructorDetour(IntPtr drawBase) - { - if (DrawObjectToObject.TryGetValue(drawBase, out ushort idx)) - { - var gameObj = GetGameObjectFromDrawObject(drawBase, idx); - if (clientState.LocalPlayer != null && gameObj == (GameObject*)clientState.LocalPlayer!.Address) - { - //PluginLog.Verbose("Clearing resources"); - //cachedResources.Clear(); - DrawObjectToObject.Clear(); - } - } - CharacterBaseDestructorHook!.Original.Invoke(drawBase); - } - - private void DisableHumanHooks() - { - ResolveMdlPathHook?.Disable(); - ResolveMdlPathHook?.Disable(); - ResolveMtrlPathHook?.Disable(); - EnableDrawHook?.Disable(); - LoadMtrlTexHook?.Disable(); - CharacterBaseCreateHook?.Disable(); - CharacterBaseDestructorHook?.Disable(); - } - - private void DisposeHumanHooks() - { - ResolveMdlPathHook?.Dispose(); - ResolveMtrlPathHook?.Dispose(); - EnableDrawHook?.Dispose(); - LoadMtrlTexHook?.Dispose(); - CharacterBaseCreateHook?.Dispose(); - CharacterBaseDestructorHook?.Dispose(); - } - - private void EnableDrawDetour(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d) - { - var oldObject = lastGameObject; - lastGameObject = (GameObject*)gameObject; - EnableDrawHook!.Original.Invoke(gameObject, b, c, d); - lastGameObject = oldObject; - } - - private void EnableHumanHooks() - { - if (ResolveMdlPathHook?.IsEnabled ?? false) return; - - ResolveMdlPathHook?.Enable(); - ResolveMtrlPathHook?.Enable(); - EnableDrawHook?.Enable(); - LoadMtrlTexHook?.Enable(); - CharacterBaseCreateHook?.Enable(); - CharacterBaseDestructorHook?.Enable(); - } - - private string? GetCardName() - { - var uiModule = (UIModule*)gameGui.GetUIModule(); - var agentModule = uiModule->GetAgentModule(); - var agent = (byte*)agentModule->GetAgentByInternalID(393); - if (agent == null) - { - return null; - } - - var data = *(byte**)(agent + 0x28); - if (data == null) - { - return null; - } - - var block = data + 0x7A; - return new Utf8String(block).ToString(); - } - - private GameObject* GetGameObjectFromDrawObject(IntPtr drawObject, int gameObjectIdx) - { - var tmp = objectTable[gameObjectIdx]; - GameObject* gameObject; - if (tmp != null) - { - gameObject = (GameObject*)tmp.Address; - if (gameObject->DrawObject == (DrawObject*)drawObject) - { - return gameObject; - } - } - - DrawObjectToObject.Remove(drawObject); - return null; - } - - private string? GetGlamourName() - { - var addon = gameGui.GetAddonByName("MiragePrismMiragePlate", 1); - return addon == IntPtr.Zero ? null : GetPlayerName(); - } - - private string? GetInspectName() - { - var addon = gameGui.GetAddonByName("CharacterInspect", 1); - if (addon == IntPtr.Zero) - { - return null; - } - - var ui = (AtkUnitBase*)addon; - if (ui->UldManager.NodeListCount < 60) - { - return null; - } - - var text = (AtkTextNode*)ui->UldManager.NodeList[59]; - if (text == null || !text->AtkResNode.IsVisible) - { - text = (AtkTextNode*)ui->UldManager.NodeList[60]; - } - - return text != null ? text->NodeText.ToString() : null; - } - - private string GetPlayerName() - { - return clientState.LocalPlayer!.Name.ToString(); - } - - private void LoadMtrlHelper(IntPtr mtrlResourceHandle) - { - if (mtrlResourceHandle == IntPtr.Zero) - { - return; - } - - try - { - var mtrl = (MtrlResource*)mtrlResourceHandle; - var mtrlPath = Utf8String.FromSpanUnsafe(mtrl->Handle.FileNameSpan(), true, null, true); - PluginLog.Verbose("Attempting to resolve: " + mtrlPath.ToString()); - var mtrlResource = factory.Create(mtrlPath.ToString()); - var existingMat = loadedMaterials.FirstOrDefault(m => m.IsReplacedByThis(mtrlResource)); - if (existingMat != null) - { - PluginLog.Verbose("Resolving material: " + existingMat.GamePath); - for (int i = 0; i < mtrl->NumTex; i++) - { - var texPath = new Utf8String(mtrl->TexString(i)); - PluginLog.Verbose("Resolving tex: " + texPath.ToString()); - - AddRequestedResource(factory.Create(texPath.ToString())); - } - - loadedMaterials = new(loadedMaterials.Except(new[] { existingMat })); - } - } - catch (Exception ex) - { - PluginLog.Error(ex, "error"); - } - } - - private byte LoadMtrlTexDetour(IntPtr mtrlResourceHandle) - { - LoadMtrlHelper(mtrlResourceHandle); - var ret = LoadMtrlTexHook!.Original(mtrlResourceHandle); - return ret; - } - - private IntPtr ResolveMdlDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType) - => ResolvePathDetour(drawObject, ResolveMdlPathHook!.Original(drawObject, path, unk3, modelType)); - - private IntPtr ResolveMtrlDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5) - => ResolvePathDetour(drawObject, ResolveMtrlPathHook!.Original(drawObject, path, unk3, unk4, unk5)); - - private unsafe IntPtr ResolvePathDetour(IntPtr drawObject, IntPtr path) - { - if (path == IntPtr.Zero || clientState.LocalPlayer == null) - { - return path; - } - - var gamepath = new Utf8String((byte*)path); - - var playerName = GetPlayerName(); - var gameDrawObject = (DrawObject*)drawObject; - GameObject* gameObject = lastGameObject; - - if (DrawObjectToObject.TryGetValue(drawObject, out ushort idx)) - { - gameObject = GetGameObjectFromDrawObject(drawObject, DrawObjectToObject[drawObject]); - } - - if (gameObject != null && (gameObject->DrawObject == null || gameObject->DrawObject == gameDrawObject)) - { - // 240, 241, 242 and 243 might need Penumbra config readout - var actualName = gameObject->ObjectIndex switch - { - 240 => GetPlayerName(), // character window - 241 => GetInspectName() ?? GetCardName() ?? GetGlamourName(), // inspect, character card, glamour plate editor. - 242 => GetPlayerName(), // try-on - 243 => GetPlayerName(), // dye preview - _ => null, - } ?? new Utf8String(gameObject->Name).ToString(); - - if (actualName != playerName) - { - return path; - } - - PluginLog.Verbose("Resolving resource: " + gamepath.ToString()); - PlayerLoadEvent?.Invoke((IntPtr)gameObject, new EventArgs()); - - var resource = factory.Create(gamepath.ToString()); - - if (gamepath.ToString().EndsWith("mtrl")) - { - loadedMaterials.Add(resource); - } - - AddRequestedResource(resource); - } - - return path; - } - - private void SetupHumanHooks() - { - if (ResolveMdlPathHook != null) return; - - ResolveMdlPathHook = new Hook(DrawObjectHumanVTable[ResolveMdlIdx], ResolveMdlDetour); - ResolveMtrlPathHook = new Hook(DrawObjectHumanVTable[ResolveMtrlIdx], ResolveMtrlDetour); - } - } -} diff --git a/MareSynchronos/Managers/CharacterManager.cs b/MareSynchronos/Managers/CharacterManager.cs index 1f628c7..ced5000 100644 --- a/MareSynchronos/Managers/CharacterManager.cs +++ b/MareSynchronos/Managers/CharacterManager.cs @@ -4,15 +4,21 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using MareSynchronos.Hooks; +using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using MareSynchronos.Factories; using MareSynchronos.Models; using MareSynchronos.Utils; using MareSynchronos.WebAPI; using Newtonsoft.Json; +using Penumbra.GameData.ByteString; +using Penumbra.Interop.Structs; +using Penumbra.PlayerWatch; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using System.Text; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; @@ -20,26 +26,37 @@ namespace MareSynchronos.Managers { public class CharacterManager : IDisposable { - private readonly DrawHooks drawHooks; private readonly ClientState clientState; private readonly Framework framework; private readonly ApiController apiController; private readonly ObjectTable objectTable; private readonly IpcManager ipcManager; - private Task? drawHookTask = null; + private readonly FileReplacementFactory factory; + private readonly IPlayerWatcher watcher; + private Task? playerChangedTask = null; - public CharacterManager(DrawHooks drawhooks, ClientState clientState, Framework framework, ApiController apiController, ObjectTable objectTable, IpcManager ipcManager) + public CharacterManager(ClientState clientState, Framework framework, ApiController apiController, ObjectTable objectTable, IpcManager ipcManager, FileReplacementFactory factory) { - this.drawHooks = drawhooks; this.clientState = clientState; this.framework = framework; this.apiController = apiController; this.objectTable = objectTable; this.ipcManager = ipcManager; - drawHooks.StartHooks(); + this.factory = factory; + watcher = PlayerWatchFactory.Create(framework, clientState, objectTable); clientState.TerritoryChanged += ClientState_TerritoryChanged; framework.Update += Framework_Update; - drawhooks.PlayerLoadEvent += Drawhooks_PlayerLoadEvent; + ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent; + } + + private void IpcManager_PenumbraRedrawEvent(object? sender, EventArgs e) + { + var actorName = ((string)sender!); + PluginLog.Debug("Penumbra redraw " + actorName); + if (actorName == GetPlayerName()) + { + PlayerChanged(actorName); + } } Dictionary localPlayers = new(); @@ -81,6 +98,118 @@ namespace MareSynchronos.Managers } } + private string GetPlayerName() + { + return clientState.LocalPlayer!.Name.ToString(); + } + + private void Watcher_PlayerChanged(Dalamud.Game.ClientState.Objects.Types.Character actor) + { + if (actor.Name.ToString() == clientState.LocalPlayer!.Name.ToString()) + { + PlayerChanged(actor.Name.ToString()); + } + else + { + PluginLog.Debug("PlayerChanged: " + actor.Name.ToString()); + } + } + + private unsafe void PlayerChanged(string name) + { + //if (sender == null) return; + PluginLog.Debug("Player changed: " + name); + if (playerChangedTask is { IsCompleted: false }) return; + + playerChangedTask = Task.Run(() => + { + var obj = (GameObject*)clientState.LocalPlayer!.Address; + + PluginLog.Debug("Waiting for charater to be drawn"); + while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something + { + PluginLog.Debug("Waiting for character to finish drawing"); + Thread.Sleep(10); + } + PluginLog.Debug("Character finished drawing"); + + // wait half a second just in case + Thread.Sleep(500); + + var cache = CreateFullCharacterCache(); + while (!cache.IsCompleted) + { + Thread.Sleep(50); + } + + _ = apiController.SendCharacterData(cache.Result); + }); + } + + public unsafe CharacterCache BuildCharacterCache() + { + var cache = new CharacterCache(); + + var model = (CharacterBase*)((Character*)clientState.LocalPlayer!.Address)->GameObject.GetDrawObject(); + for (var idx = 0; idx < model->SlotCount; ++idx) + { + var mdl = (RenderModel*)model->ModelArray[idx]; + if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) + { + continue; + } + + var mdlPath = new Utf8String(mdl->ResourceHandle->FileName()).ToString(); + + FileReplacement cachedMdlResource = factory.Create(); + cachedMdlResource.GamePaths = ipcManager.PenumbraReverseResolvePath(mdlPath, GetPlayerName()); + cachedMdlResource.SetResolvedPath(mdlPath); + PluginLog.Verbose("Resolving for model " + mdlPath); + + cache.AddAssociatedResource(cachedMdlResource, null!, null!); + + var imc = (ResourceHandle*)model->IMCArray[idx]; + if (imc != null) + { + byte[] imcData = new byte[imc->Data->DataLength / sizeof(long)]; + Marshal.Copy((IntPtr)imc->Data->DataPtr, imcData, 0, (int)imc->Data->DataLength / sizeof(long)); + string imcDataStr = BitConverter.ToString(imcData).Replace("-", ""); + cachedMdlResource.ImcData = imcDataStr; + } + cache.AddAssociatedResource(cachedMdlResource, null!, null!); + + for (int mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) + { + var mtrl = (Material*)mdl->Materials[mtrlIdx]; + if (mtrl == null) continue; + + //var mtrlFileResource = factory.Create(); + var mtrlPath = new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2]; + PluginLog.Verbose("Resolving for material " + mtrlPath); + var cachedMtrlResource = factory.Create(); + cachedMtrlResource.GamePaths = ipcManager.PenumbraReverseResolvePath(mtrlPath, GetPlayerName()); + cachedMtrlResource.SetResolvedPath(mtrlPath); + cache.AddAssociatedResource(cachedMtrlResource, cachedMdlResource, null!); + + var mtrlResource = (MtrlResource*)mtrl->ResourceHandle; + for (int resIdx = 0; resIdx < mtrlResource->NumTex; resIdx++) + { + var texPath = new Utf8String(mtrlResource->TexString(resIdx)).ToString(); + + if (string.IsNullOrEmpty(texPath.ToString())) continue; + PluginLog.Verbose("Resolving for texture " + texPath); + + var cachedTexResource = factory.Create(); + cachedTexResource.GamePaths = new[] { texPath }; + cachedTexResource.SetResolvedPath(ipcManager.PenumbraResolvePath(texPath, GetPlayerName())!); + cache.AddAssociatedResource(cachedTexResource, cachedMdlResource, cachedMtrlResource); + } + } + } + + return cache; + } + private void ClientState_TerritoryChanged(object? sender, ushort e) { localPlayers.Clear(); @@ -89,46 +218,31 @@ namespace MareSynchronos.Managers public void Dispose() { framework.Update -= Framework_Update; - drawHooks.PlayerLoadEvent -= Drawhooks_PlayerLoadEvent; clientState.TerritoryChanged -= ClientState_TerritoryChanged; - drawHooks?.Dispose(); + watcher.PlayerChanged -= Watcher_PlayerChanged; + watcher?.Dispose(); } - private unsafe void Drawhooks_PlayerLoadEvent(object? sender, EventArgs e) + internal void StartWatchingPlayer() { - if (sender == null) return; - if (drawHookTask != null && !drawHookTask.IsCompleted) return; - - var obj = (GameObject*)(IntPtr)sender; - drawHookTask = Task.Run(() => - { - PluginLog.Debug("Waiting for charater to be drawn"); - while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something - { - Thread.Sleep(10); - } - PluginLog.Debug("Character finished drawing"); - - // wait one more second just in case - Thread.Sleep(1000); - - var cache = CreateFullCharacterCache(); - while (!cache.IsCompleted) - { - Task.Delay(50); - } - - _ = apiController.SendCharacterData(cache.Result); - }); + watcher.AddPlayerToWatch(clientState.LocalPlayer!.Name.ToString()); + watcher.PlayerChanged += Watcher_PlayerChanged; + watcher.Enable(); } - public CharacterCache GetCharacterCache() => drawHooks.BuildCharacterCache(); + public void StopWatchPlayer(string name) + { + watcher.RemovePlayerFromWatch(name); + } - public void PrintRequestedResources() => drawHooks.PrintRequestedResources(); + public void WatchPlayer(string name) + { + watcher.AddPlayerToWatch(name); + } private async Task CreateFullCharacterCache() { - var cache = drawHooks.BuildCharacterCache(); + var cache = BuildCharacterCache(); cache.SetGlamourerData(ipcManager.GlamourerGetCharacterCustomization()!); cache.JobId = clientState.LocalPlayer!.ClassJob.Id; await Task.Run(async () => @@ -137,6 +251,7 @@ namespace MareSynchronos.Managers { await Task.Delay(50); } + var json = JsonConvert.SerializeObject(cache, Formatting.Indented); cache.CacheHash = Crypto.GetHash(json); @@ -145,12 +260,12 @@ namespace MareSynchronos.Managers return cache; } - public void DebugJson() + public async Task DebugJson() { var cache = CreateFullCharacterCache(); while (!cache.IsCompleted) { - Task.Delay(50); + await Task.Delay(50); } PluginLog.Debug(JsonConvert.SerializeObject(cache.Result, Formatting.Indented)); diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs index 5ccb7ba..3d1116d 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Managers/IpcManager.cs @@ -15,10 +15,14 @@ namespace MareSynchronos.Managers private ICallGateSubscriber? glamourerApplyCharacterCustomization; private ICallGateSubscriber penumbraApiVersion; private ICallGateSubscriber glamourerApiVersion; + private ICallGateSubscriber penumbraObjectIsRedrawn; private ICallGateSubscriber? penumbraRedraw; + private ICallGateSubscriber? penumbraReverseResolvePath; public bool Initialized { get; private set; } = false; + public event EventHandler? PenumbraRedrawEvent; + public IpcManager(DalamudPluginInterface pi) { pluginInterface = pi; @@ -29,9 +33,12 @@ namespace MareSynchronos.Managers penumbraRedraw = pluginInterface.GetIpcSubscriber("Penumbra.RedrawObjectByName"); glamourerGetCharacterCustomization = pluginInterface.GetIpcSubscriber("Glamourer.GetCharacterCustomization"); glamourerApplyCharacterCustomization = pluginInterface.GetIpcSubscriber("Glamourer.ApplyCharacterCustomization"); + penumbraReverseResolvePath = pluginInterface.GetIpcSubscriber("Penumbra.ReverseResolvePath"); penumbraApiVersion = pluginInterface.GetIpcSubscriber("Penumbra.ApiVersion"); glamourerApiVersion = pluginInterface.GetIpcSubscriber("Glamourer.ApiVersion"); - penumbraInit.Subscribe(() => penumbraRedraw!.InvokeAction("self", 0)); + penumbraObjectIsRedrawn = pluginInterface.GetIpcSubscriber("Penumbra.ObjectIsRedrawn"); + penumbraObjectIsRedrawn.Subscribe(RedrawEvent); + penumbraInit.Subscribe(RedrawSelf); Initialized = true; } @@ -60,16 +67,35 @@ namespace MareSynchronos.Managers } } + private void RedrawEvent(string actorName) + { + PenumbraRedrawEvent?.Invoke(actorName, EventArgs.Empty); + } + + private void RedrawSelf() + { + penumbraRedraw!.InvokeAction("self", 0); + } + private void Uninitialize() { + penumbraInit.Unsubscribe(RedrawSelf); + penumbraObjectIsRedrawn.Unsubscribe(RedrawEvent); penumbraResolvePath = null; penumbraResolveModDir = null; glamourerGetCharacterCustomization = null; glamourerApplyCharacterCustomization = null; + penumbraReverseResolvePath = null; Initialized = false; PluginLog.Debug("IPC Manager disposed"); } + public string[] PenumbraReverseResolvePath(string path, string characterName) + { + if (!CheckPenumbraAPI()) return new[] { path }; + return penumbraReverseResolvePath!.InvokeFunc(path, characterName); + } + public string? PenumbraResolvePath(string path, string characterName) { if (!CheckPenumbraAPI()) return null; diff --git a/MareSynchronos/Models/CharacterCache.cs b/MareSynchronos/Models/CharacterCache.cs index 45331d4..694c5e7 100644 --- a/MareSynchronos/Models/CharacterCache.cs +++ b/MareSynchronos/Models/CharacterCache.cs @@ -13,10 +13,10 @@ namespace MareSynchronos.Models { [JsonProperty] public List AllReplacements => - FileReplacements.Where(x => x.HasFileReplacement) - .Concat(FileReplacements.SelectMany(f => f.Associated).Where(f => f.HasFileReplacement)) - .Concat(FileReplacements.SelectMany(f => f.Associated).SelectMany(f => f.Associated).Where(f => f.HasFileReplacement)) - .Distinct().OrderBy(f => f.GamePath) + FileReplacements.Where(f => f.HasFileReplacement) + .Concat(FileReplacements.SelectMany(f => f.Associated)).Where(f => f.HasFileReplacement) + .Concat(FileReplacements.SelectMany(f => f.Associated).SelectMany(f => f.Associated)).Where(f => f.HasFileReplacement) + .Distinct().OrderBy(f => f.GamePaths[0]) .ToList(); public List FileReplacements { get; set; } = new List(); @@ -31,11 +31,10 @@ namespace MareSynchronos.Models [JsonProperty] public uint JobId { get; set; } = 0; - public void AddAssociatedResource(FileReplacement resource, FileReplacement mdlParent, FileReplacement mtrlParent) + public void AddAssociatedResource(FileReplacement resource, FileReplacement? mdlParent, FileReplacement? mtrlParent) { try { - if (resource == null) return; if (mdlParent == null) { resource.IsInUse = true; @@ -43,16 +42,16 @@ namespace MareSynchronos.Models return; } - FileReplacement replacement; - - if (mtrlParent == null && (replacement = FileReplacements.SingleOrDefault(f => f == mdlParent)!) != null) + var mdlReplacements = FileReplacements.Where(f => f == mdlParent && mtrlParent == null); + foreach (var mdlReplacement in mdlReplacements) { - replacement.AddAssociated(resource); + mdlReplacement.AddAssociated(resource); } - if ((replacement = FileReplacements.SingleOrDefault(f => f == mdlParent)?.Associated.SingleOrDefault(f => f == mtrlParent)!) != null) + var mtrlReplacements = FileReplacements.Where(f => f == mdlParent).SelectMany(a => a.Associated).Where(f => f == mtrlParent); + foreach (var mtrlReplacement in mtrlReplacements) { - replacement.AddAssociated(resource); + mtrlReplacement.AddAssociated(resource); } } catch (Exception ex) @@ -89,7 +88,7 @@ namespace MareSynchronos.Models public override string ToString() { StringBuilder stringBuilder = new(); - foreach (var fileReplacement in FileReplacements.OrderBy(a => a.GamePath)) + foreach (var fileReplacement in FileReplacements.OrderBy(a => a.GamePaths[0])) { stringBuilder.AppendLine(fileReplacement.ToString()); } diff --git a/MareSynchronos/Models/FileReplacement.cs b/MareSynchronos/Models/FileReplacement.cs index 36686bc..12cb4ce 100644 --- a/MareSynchronos/Models/FileReplacement.cs +++ b/MareSynchronos/Models/FileReplacement.cs @@ -17,21 +17,21 @@ namespace MareSynchronos.Models private readonly string penumbraDirectory; [JsonProperty] - public string GamePath { get; private set; } - public string ResolvedPath { get; private set; } = string.Empty; + public string[] GamePaths { get; set; } = Array.Empty(); + [JsonProperty] + public string ResolvedPath { get; set; } = string.Empty; [JsonProperty] public string Hash { get; set; } = string.Empty; public bool IsInUse { get; set; } = false; public List Associated { get; set; } = new List(); [JsonProperty] public string ImcData { get; set; } = string.Empty; - public bool HasFileReplacement => GamePath != ResolvedPath; + public bool HasFileReplacement => GamePaths.Length >= 1 && GamePaths[0] != ResolvedPath; public bool Computed => (computationTask == null || (computationTask?.IsCompleted ?? true)) && Associated.All(f => f.Computed); private Task? computationTask = null; - public FileReplacement(string gamePath, string penumbraDirectory) + public FileReplacement(string penumbraDirectory) { - GamePath = gamePath; this.penumbraDirectory = penumbraDirectory; } @@ -39,15 +39,7 @@ namespace MareSynchronos.Models { fileReplacement.IsInUse = true; - if (!Associated.Any(a => a.IsReplacedByThis(fileReplacement))) - { - Associated.Add(fileReplacement); - } - } - - public void SetGamePath(string path) - { - GamePath = path; + Associated.Add(fileReplacement); } public void SetResolvedPath(string path) @@ -107,26 +99,16 @@ namespace MareSynchronos.Models return hash; } - public bool IsReplacedByThis(string path) - { - return GamePath.ToLower() == path.ToLower() || ResolvedPath.ToLower() == path.ToLower(); - } - - public bool IsReplacedByThis(FileReplacement replacement) - { - return IsReplacedByThis(replacement.GamePath) || IsReplacedByThis(replacement.ResolvedPath); - } - public override string ToString() { StringBuilder builder = new(); - builder.AppendLine($"Modded: {HasFileReplacement} - {GamePath} => {ResolvedPath}"); + builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}"); foreach (var l1 in Associated) { - builder.AppendLine($" + Modded: {l1.HasFileReplacement} - {l1.GamePath} => {l1.ResolvedPath}"); + builder.AppendLine($" + Modded: {l1.HasFileReplacement} - {string.Join(",", l1.GamePaths)} => {l1.ResolvedPath}"); foreach (var l2 in l1.Associated) { - builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {l2.GamePath} => {l2.ResolvedPath}"); + builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {string.Join(",", l2.GamePaths)} => {l2.ResolvedPath}"); } } return builder.ToString(); @@ -137,7 +119,7 @@ namespace MareSynchronos.Models if (obj == null) return true; if (obj.GetType() == typeof(FileReplacement)) { - return Hash == ((FileReplacement)obj).Hash && GamePath == ((FileReplacement)obj).GamePath; + return Hash == ((FileReplacement)obj).Hash; } return base.Equals(obj); @@ -148,8 +130,7 @@ namespace MareSynchronos.Models int result = 13; result *= 397; result += Hash.GetHashCode(); - result += GamePath.GetHashCode(); - result += ImcData.GetHashCode(); + result += ResolvedPath.GetHashCode(); return result; } diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index bb42eed..83b1b92 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -8,7 +8,6 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; -using MareSynchronos.Hooks; using Dalamud.Game; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState; @@ -29,21 +28,19 @@ namespace MareSynchronos private const string commandName = "/mare"; private readonly ClientState clientState; private readonly Framework framework; - private readonly GameGui gameGui; private readonly ObjectTable objectTable; private readonly WindowSystem windowSystem; private readonly ApiController apiController; private CharacterManager? characterManager; private IpcManager ipcManager; public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, - Framework framework, ObjectTable objectTable, ClientState clientState, GameGui gameGui) + Framework framework, ObjectTable objectTable, ClientState clientState) { this.PluginInterface = pluginInterface; this.CommandManager = commandManager; this.framework = framework; this.objectTable = objectTable; this.clientState = clientState; - this.gameGui = gameGui; Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); Configuration.Initialize(this.PluginInterface); @@ -94,8 +91,8 @@ namespace MareSynchronos } characterManager = new CharacterManager( - new DrawHooks(PluginInterface, clientState, objectTable, new FileReplacementFactory(ipcManager, clientState), gameGui), - clientState, framework, apiController, objectTable, ipcManager); + clientState, framework, apiController, objectTable, ipcManager, new FileReplacementFactory(ipcManager)); + characterManager.StartWatchingPlayer(); ipcManager.PenumbraRedraw(clientState.LocalPlayer!.Name.ToString()); }); @@ -140,11 +137,14 @@ namespace MareSynchronos File.Copy(fileCache.Filepath, newFilePath); if (resourceDict != null) { - resourceDict[replacement.GamePath] = $"files\\{fileCache.Hash.ToLower() + ext}"; + foreach(var path in replacement.GamePaths) + { + resourceDict[path] = $"files\\{fileCache.Hash.ToLower() + ext}"; + } } else { - File.AppendAllLines(Path.Combine(targetDirectory, "filelist.txt"), new[] { $"\"{replacement.GamePath}\": \"files\\\\{fileCache.Hash.ToLower() + ext}\"," }); + //File.AppendAllLines(Path.Combine(targetDirectory, "filelist.txt"), new[] { $"\"{replacement.GamePath}\": \"files\\\\{fileCache.Hash.ToLower() + ext}\"," }); } } } @@ -167,14 +167,21 @@ namespace MareSynchronos private void OnCommand(string command, string args) { - if (args == "print") - { - characterManager?.PrintRequestedResources(); - } - if (args == "printjson") { - characterManager?.DebugJson(); + _ = characterManager?.DebugJson(); + } + + if (args.StartsWith("watch")) + { + var playerName = args.Replace("watch", "").Trim(); + characterManager!.WatchPlayer(playerName); + } + + if (args.StartsWith("stop")) + { + var playerName = args.Replace("watch", "").Trim(); + characterManager!.StopWatchPlayer(playerName); } if (args == "createtestmod") @@ -199,7 +206,7 @@ namespace MareSynchronos Description = "Mare Synchronous Test Mod Export", }; - var resources = characterManager!.GetCharacterCache(); + var resources = characterManager!.BuildCharacterCache(); var metaJson = JsonConvert.SerializeObject(meta); File.WriteAllText(Path.Combine(modDirectoryPath, "meta.json"), metaJson);