diff --git a/MareSynchronos/Models/FileReplacementFactory.cs b/MareSynchronos/Factories/FileReplacementFactory.cs similarity index 78% rename from MareSynchronos/Models/FileReplacementFactory.cs rename to MareSynchronos/Factories/FileReplacementFactory.cs index 1e7e128..9c4b33d 100644 --- a/MareSynchronos/Models/FileReplacementFactory.cs +++ b/MareSynchronos/Factories/FileReplacementFactory.cs @@ -1,8 +1,10 @@ using Dalamud.Game.ClientState; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; +using MareSynchronos.FileCacheDB; +using MareSynchronos.Models; -namespace MareSynchronos.Models +namespace MareSynchronos.Factories { public class FileReplacementFactory { @@ -16,20 +18,22 @@ namespace MareSynchronos.Models penumbraDirectory = pluginInterface.GetIpcSubscriber("Penumbra.GetModDirectory").InvokeFunc().ToLower() + '\\'; this.clientState = clientState; } - public FileReplacement Create(string gamePath) + public FileReplacement Create(string gamePath, bool resolve = true) { var fileReplacement = new FileReplacement(gamePath, penumbraDirectory); - fileReplacement.SetReplacedPath(resolvePath.InvokeFunc(gamePath, clientState.LocalPlayer!.Name.ToString())); + if (!resolve) return fileReplacement; + + fileReplacement.SetResolvedPath(resolvePath.InvokeFunc(gamePath, clientState.LocalPlayer!.Name.ToString())); if (!fileReplacement.HasFileReplacement) { - // try to resolve path with -- instead? + // try to resolve path with --filename instead? string[] tempGamePath = gamePath.Split('/'); tempGamePath[tempGamePath.Length - 1] = "--" + tempGamePath[tempGamePath.Length - 1]; string newTempGamePath = string.Join('/', tempGamePath); var resolvedPath = resolvePath.InvokeFunc(newTempGamePath, clientState.LocalPlayer!.Name.ToString()); if (resolvedPath != newTempGamePath) { - fileReplacement.SetReplacedPath(resolvedPath); + fileReplacement.SetResolvedPath(resolvedPath); fileReplacement.SetGamePath(newTempGamePath); } } diff --git a/MareSynchronos/Hooks/DrawHooks.cs b/MareSynchronos/Hooks/DrawHooks.cs index 67c52c1..cf30c29 100644 --- a/MareSynchronos/Hooks/DrawHooks.cs +++ b/MareSynchronos/Hooks/DrawHooks.cs @@ -1,14 +1,17 @@ using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Gui; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Plugin; -using Dalamud.Plugin.Ipc; 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.Models; using Penumbra.GameData.ByteString; using Penumbra.Interop.Structs; @@ -17,76 +20,143 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; -using System.Text; -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; - // [Signature( "48 8D 1D ?? ?? ?? ?? 48 C7 41", ScanType = ScanType.StaticAddress )] - // public IntPtr* DrawObjectVTable; - // - // [Signature( "48 8D 05 ?? ?? ?? ?? 45 33 C0 48 89 03 BA", ScanType = ScanType.StaticAddress )] - // public IntPtr* DrawObjectDemihumanVTable; - // - // [Signature( "48 8D 05 ?? ?? ?? ?? 48 89 03 33 C0 48 89 83 ?? ?? ?? ?? 48 89 83 ?? ?? ?? ?? C7 83", ScanType = ScanType.StaticAddress )] - // public IntPtr* DrawObjectMonsterVTable; - // - // public const int ResolveRootIdx = 71; - - public const int ResolveSklbIdx = 72; - public const int ResolveMdlIdx = 73; - public const int ResolveSkpIdx = 74; - public const int ResolvePhybIdx = 75; - public const int ResolvePapIdx = 76; - public const int ResolveTmbIdx = 77; - public const int ResolveMPapIdx = 79; - public const int ResolveImcIdx = 81; - public const int ResolveMtrlIdx = 82; - public const int ResolveDecalIdx = 83; - public const int ResolveVfxIdx = 84; - public const int ResolveEidIdx = 85; - private readonly DalamudPluginInterface pluginInterface; - private readonly ClientState clientState; - private readonly ObjectTable objectTable; - private readonly FileReplacementFactory factory; - - public delegate IntPtr GeneralResolveDelegate(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4); - public delegate IntPtr MPapResolveDelegate(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5); - public delegate IntPtr MaterialResolveDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5); - public delegate IntPtr EidResolveDelegate(IntPtr drawObject, IntPtr path, IntPtr unk3); - - public delegate void OnModelLoadCompleteDelegate(IntPtr drawObject); - - public Hook? ResolveDecalPathHook; - public Hook? ResolveEidPathHook; - public Hook? ResolveImcPathHook; - public Hook? ResolveMPapPathHook; public Hook? ResolveMdlPathHook; public Hook? ResolveMtrlPathHook; - public Hook? ResolvePapPathHook; - public Hook? ResolvePhybPathHook; - public Hook? ResolveSklbPathHook; - public Hook? ResolveSkpPathHook; - public Hook? ResolveTmbPathHook; - public Hook? ResolveVfxPathHook; + 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 ConcurrentBag cachedResources = new(); + private GameObject* lastGameObject = null; + private ConcurrentBag loadedMaterials = new(); + private CharacterCache characterCache; - public DrawHooks(DalamudPluginInterface pluginInterface, ClientState clientState, ObjectTable objectTable, FileReplacementFactory factory) + 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; + characterCache = new CharacterCache(); 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.IsInUse = false; + resource.ImcData = string.Empty; + } + + var cache = new CharacterCache(); + + PluginLog.Debug("Invaldated character cache"); + + 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.IsReplacedByThis(mdlResource)); + var imc = (ResourceHandle*)model->IMCArray[idx]; + if (imc != null) + { + byte[] imcData = new byte[imc->Data->DataLength]; + Marshal.Copy((IntPtr)imc->Data->DataPtr, imcData, 0, (int)imc->Data->DataLength); + 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.IsReplacedByThis(mtrlFileResource)); + 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.IsReplacedByThis(texResource)); + cache.AddAssociatedResource(cachedTexResource, cachedMdlResource, cachedMtrlResource); + } + } + } + + return cache; + } + + public List PrintRequestedResources() + { + var cache = BuildCharacterCache(); + + PluginLog.Debug("--- CURRENTLY LOADED FILES ---"); + + PluginLog.Debug(cache.ToString()); + + PluginLog.Debug("--- LOOSE FILES ---"); + + foreach (var resource in cachedResources.Where(r => !r.IsInUse).OrderBy(a => a.GamePath)) + { + PluginLog.Debug(resource.ToString()); + } + + return cache.FileReplacements; + } + public void StartHooks() { - allRequestedResources.Clear(); + cachedResources.Clear(); SetupHumanHooks(); EnableHumanHooks(); PluginLog.Debug("Hooks enabled"); @@ -98,153 +168,151 @@ namespace MareSynchronos.Hooks DisposeHumanHooks(); } - private void SetupHumanHooks() + private void AddRequestedResource(FileReplacement replacement) { - if (ResolveDecalPathHook != null) return; - - ResolveDecalPathHook = new Hook(DrawObjectHumanVTable[ResolveDecalIdx], ResolveDecalDetour); - ResolveEidPathHook = new Hook(DrawObjectHumanVTable[ResolveEidIdx], ResolveEidDetour); - ResolveImcPathHook = new Hook(DrawObjectHumanVTable[ResolveImcIdx], ResolveImcDetour); - ResolveMPapPathHook = new Hook(DrawObjectHumanVTable[ResolveMPapIdx], ResolveMPapDetour); - ResolveMdlPathHook = new Hook(DrawObjectHumanVTable[ResolveMdlIdx], ResolveMdlDetour); - ResolveMtrlPathHook = new Hook(DrawObjectHumanVTable[ResolveMtrlIdx], ResolveMtrlDetour); - ResolvePapPathHook = new Hook(DrawObjectHumanVTable[ResolvePapIdx], ResolvePapDetour); - ResolvePhybPathHook = new Hook(DrawObjectHumanVTable[ResolvePhybIdx], ResolvePhybDetour); - ResolveSklbPathHook = new Hook(DrawObjectHumanVTable[ResolveSklbIdx], ResolveSklbDetour); - ResolveSkpPathHook = new Hook(DrawObjectHumanVTable[ResolveSkpIdx], ResolveSkpDetour); - ResolveTmbPathHook = new Hook(DrawObjectHumanVTable[ResolveTmbIdx], ResolveTmbDetour); - ResolveVfxPathHook = new Hook(DrawObjectHumanVTable[ResolveVfxIdx], ResolveVfxDetour); + if (!cachedResources.Any(a => a.IsReplacedByThis(replacement))) + { + cachedResources.Add(replacement); + } } - private void EnableHumanHooks() + private IntPtr CharacterBaseCreateDetour(uint a, IntPtr b, IntPtr c, byte d) { - if (ResolveDecalPathHook?.IsEnabled ?? false) return; + PluginLog.Debug("Character base detour"); + var ret = CharacterBaseCreateHook!.Original(a, b, c, d); + if (lastGameObject != null) + { + DrawObjectToObject[ret] = (lastGameObject->ObjectIndex); + } - ResolveDecalPathHook?.Enable(); - //ResolveEidPathHook?.Enable(); - //ResolveImcPathHook?.Enable(); - //ResolveMPapPathHook?.Enable(); - ResolveMdlPathHook?.Enable(); - ResolveMtrlPathHook?.Enable(); - //ResolvePapPathHook?.Enable(); - //ResolvePhybPathHook?.Enable(); - //ResolveSklbPathHook?.Enable(); - //ResolveSkpPathHook?.Enable(); - //ResolveTmbPathHook?.Enable(); - //ResolveVfxPathHook?.Enable(); - EnableDrawHook?.Enable(); - LoadMtrlTexHook?.Enable(); + return ret; + } + + private void CharacterBaseDestructorDetour(IntPtr drawBase) + { + if (DrawObjectToObject.TryGetValue(drawBase, out ushort idx)) + { + var gameObj = GetGameObjectFromDrawObject(drawBase, idx); + if (gameObj == (GameObject*)clientState.LocalPlayer!.Address) + { + PluginLog.Debug("Clearing resources"); + cachedResources.Clear(); + DrawObjectToObject.Clear(); + } + } + CharacterBaseDestructorHook!.Original.Invoke(drawBase); } private void DisableHumanHooks() { - ResolveDecalPathHook?.Disable(); - //ResolveEidPathHook?.Disable(); - //ResolveImcPathHook?.Disable(); - //ResolveMPapPathHook?.Disable(); + ResolveMdlPathHook?.Disable(); ResolveMdlPathHook?.Disable(); ResolveMtrlPathHook?.Disable(); - //ResolvePapPathHook?.Disable(); - //ResolvePhybPathHook?.Disable(); - //ResolveSklbPathHook?.Disable(); - //ResolveSkpPathHook?.Disable(); - //ResolveTmbPathHook?.Disable(); - //ResolveVfxPathHook?.Disable(); EnableDrawHook?.Disable(); LoadMtrlTexHook?.Disable(); + CharacterBaseCreateHook?.Disable(); + CharacterBaseDestructorHook?.Disable(); } private void DisposeHumanHooks() { - ResolveDecalPathHook?.Dispose(); - //ResolveEidPathHook?.Dispose(); - //ResolveImcPathHook?.Dispose(); - //ResolveMPapPathHook?.Dispose(); ResolveMdlPathHook?.Dispose(); ResolveMtrlPathHook?.Dispose(); - //ResolvePapPathHook?.Dispose(); - //ResolvePhybPathHook?.Dispose(); - //ResolveSklbPathHook?.Dispose(); - //ResolveSkpPathHook?.Dispose(); - //ResolveTmbPathHook?.Dispose(); - //ResolveVfxPathHook?.Dispose(); EnableDrawHook?.Dispose(); LoadMtrlTexHook?.Dispose(); + CharacterBaseCreateHook?.Dispose(); + CharacterBaseDestructorHook?.Dispose(); } - // Humans - private IntPtr ResolveDecalDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4) - => ResolvePathDetour(drawObject, ResolveDecalPathHook!.Original(drawObject, path, unk3, unk4)); - - private IntPtr ResolveEidDetour(IntPtr drawObject, IntPtr path, IntPtr unk3) - => ResolvePathDetour(drawObject, ResolveEidPathHook!.Original(drawObject, path, unk3)); - - private IntPtr ResolveImcDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4) - => ResolvePathDetour(drawObject, ResolveImcPathHook!.Original(drawObject, path, unk3, unk4)); - - private IntPtr ResolveMPapDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, uint unk5) - => ResolvePathDetour(drawObject, ResolveMPapPathHook!.Original(drawObject, path, unk3, unk4, unk5)); - - private IntPtr ResolveMdlDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint modelType) - { - return 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 IntPtr ResolvePapDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5) - { - return ResolvePathDetour(drawObject, ResolvePapPathHook!.Original(drawObject, path, unk3, unk4, unk5)); - } - - private IntPtr ResolvePhybDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4) - { - return ResolvePathDetour(drawObject, ResolvePhybPathHook!.Original(drawObject, path, unk3, unk4)); - } - - private IntPtr ResolveSklbDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4) - { - return ResolvePathDetour(drawObject, ResolveSklbPathHook!.Original(drawObject, path, unk3, unk4)); - } - - private IntPtr ResolveSkpDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4) - { - return ResolvePathDetour(drawObject, ResolveSkpPathHook!.Original(drawObject, path, unk3, unk4)); - } - - private IntPtr ResolveTmbDetour(IntPtr drawObject, IntPtr path, IntPtr unk3) - => ResolvePathDetour(drawObject, ResolveTmbPathHook!.Original(drawObject, path, unk3)); - - private IntPtr ResolveVfxDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5) - => ResolvePathDetour(drawObject, ResolveVfxPathHook!.Original(drawObject, path, unk3, unk4, unk5)); - - public delegate void EnableDrawDelegate(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d); - - [Signature("E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? 33 D2 E8 ?? ?? ?? ?? 84 C0")] - public Hook? EnableDrawHook; - - public GameObject* LastGameObject { get; private set; } - private void EnableDrawDetour(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d) { - //PluginLog.Debug("Draw start"); - var oldObject = LastGameObject; - LastGameObject = (GameObject*)gameObject; + var oldObject = lastGameObject; + lastGameObject = (GameObject*)gameObject; EnableDrawHook!.Original.Invoke(gameObject, b, c, d); - LastGameObject = oldObject; - //PluginLog.Debug("Draw end"); + lastGameObject = oldObject; } - public delegate byte LoadMtrlFilesDelegate(IntPtr mtrlResourceHandle); - [Signature("4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = "LoadMtrlTexDetour")] - public Hook? LoadMtrlTexHook; - - private byte LoadMtrlTexDetour(IntPtr mtrlResourceHandle) + private void EnableHumanHooks() { - LoadMtrlHelper(mtrlResourceHandle); - var ret = LoadMtrlTexHook!.Original(mtrlResourceHandle); - return ret; + 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) @@ -268,7 +336,7 @@ namespace MareSynchronos.Hooks AddRequestedResource(factory.Create(texPath.ToString())); } - loadedMaterials.Remove(existingMat); + loadedMaterials = new(loadedMaterials.Except(new[] { existingMat })); } } catch (Exception ex) @@ -277,6 +345,19 @@ namespace MareSynchronos.Hooks } } + 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) @@ -286,23 +367,34 @@ namespace MareSynchronos.Hooks var gamepath = new Utf8String((byte*)path); - var playerName = clientState.LocalPlayer.Name.ToString(); + var playerName = GetPlayerName(); var gameDrawObject = (DrawObject*)drawObject; - var playerDrawObject = ((Character*)clientState.LocalPlayer.Address)->GameObject.GetDrawObject(); + GameObject* gameObject = lastGameObject; - if (LastGameObject != null && (LastGameObject->DrawObject == null || LastGameObject->DrawObject == gameDrawObject)) + if (DrawObjectToObject.TryGetValue(drawObject, out ushort idx)) { - var owner = new Utf8String(LastGameObject->Name).ToString(); - if (owner != playerName) + 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; } - AddRequestedResource(factory.Create(gamepath.ToString())); - } - else if (playerDrawObject == gameDrawObject) - { var resource = factory.Create(gamepath.ToString()); + if (gamepath.ToString().EndsWith("mtrl")) { loadedMaterials.Add(resource); @@ -314,140 +406,12 @@ namespace MareSynchronos.Hooks return path; } - List loadedMaterials = new(); - ConcurrentBag allRequestedResources = new(); - - public List PrintRequestedResources() + private void SetupHumanHooks() { - foreach (var resource in allRequestedResources) - { - PluginLog.Debug(resource.ToString()); - } - //PluginLog.Debug("---"); + if (ResolveMdlPathHook != null) return; - var model = (CharacterBase*)((Character*)clientState.LocalPlayer!.Address)->GameObject.GetDrawObject(); - - List fluctuatingResources = new(); - - for (var i = 0; i < model->SlotCount; ++i) - { - var mdl = (RenderModel*)model->ModelArray[i]; - - if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) - { - continue; - } - - var resource = factory.Create(new Utf8String(mdl->ResourceHandle->FileName()).ToString()); - - PluginLog.Debug("Checking model: " + resource); - - var mdlResourceRepl = allRequestedResources.FirstOrDefault(r => r.IsReplacedByThis(resource)); - - if (mdlResourceRepl != null) - { - //PluginLog.Debug("Fluctuating resource detected: " + mdlResourceRepl); - //allRequestedResources.Remove(mdlResourceRepl); - fluctuatingResources.Add(mdlResourceRepl); - } - else - { - //var resolvedPath = ResolvePath(mdlFile); - //if (resolvedPath != mdlFile) - //{ - //fluctuatingResources[mdlFile] = resolvedPath; - //} - } - - for (int mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) - { - var mtrl = (Material*)mdl->Materials[mtrlIdx]; - - if (mtrl == null) continue; - - var mtrlresource = factory.Create(new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2]); - - var mtrlResourceRepl = allRequestedResources.FirstOrDefault(r => r.IsReplacedByThis(mtrlresource)); - if (mtrlResourceRepl != null) - { - mdlResourceRepl.AddAssociated(mtrlResourceRepl); - //PluginLog.Debug("Fluctuating resource detected: " + mtrlResourceRepl); - //allRequestedResources.Remove(mtrlResourceRepl); - //fluctuatingResources.Add(mtrlResourceRepl); - } - else - { - //var resolvedPath = ResolvePath(mtrlPath); - //if (resolvedPath != mtrlPath) - //{ - // fluctuatingResources[mtrlPath] = resolvedPath; - //} - } - - var mtrlResource = (MtrlResource*)mtrl->ResourceHandle; - - for (int resIdx = 0; resIdx < mtrlResource->NumTex; resIdx++) - { - var path = new Utf8String(mtrlResource->TexString(resIdx)); - var gamePath = Utf8GamePath.FromString(path.ToString(), out var p, true) ? p : Utf8GamePath.Empty; - - var texResource = factory.Create(path.ToString()); - - var texResourceRepl = allRequestedResources.FirstOrDefault(r => r.IsReplacedByThis(texResource)); - if (texResourceRepl != null) - { - //PluginLog.Debug("Fluctuating resource detected: " + texResourceRepl); - //allRequestedResources.Remove(texResourceRepl); - //fluctuatingResources.Add(texResourceRepl); - mtrlResourceRepl.AddAssociated(texResourceRepl); - //fluctuatingResources[existingResource.Key] = existingResource.Value; - } - else - { - //var resolvedPath = ResolvePath(path.ToString()); - //if (resolvedPath != path.ToString()) - //{ - // fluctuatingResources[path.ToString()] = resolvedPath; - //} - } - } - } - } - - PluginLog.Debug("---"); - - foreach (var resource in fluctuatingResources.OrderBy(a => a.GamePath)) - { - PluginLog.Debug(Environment.NewLine + resource.ToString()); - } - - PluginLog.Debug("---"); - - /*foreach (var resource in allRequestedResources.Where(r => r.HasFileReplacement && r.Associated.Count == 0).OrderBy(a => a.GamePath)) - { - PluginLog.Debug(resource.ToString()); - }*/ - - return fluctuatingResources; - } - - private void AddRequestedResource(FileReplacement replacement) - { - if (allRequestedResources.Any(a => a.IsReplacedByThis(replacement))) - { - PluginLog.Debug("Already added: " + replacement); - return; - } - - PluginLog.Debug("Adding: " + replacement.GamePath); - - allRequestedResources.Add(replacement); - } - - public void Dispose() - { - DisableHumanHooks(); - DisposeHumanHooks(); + ResolveMdlPathHook = new Hook(DrawObjectHumanVTable[ResolveMdlIdx], ResolveMdlDetour); + ResolveMtrlPathHook = new Hook(DrawObjectHumanVTable[ResolveMtrlIdx], ResolveMtrlDetour); } } } diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index 31eb24f..85613cb 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -32,6 +32,7 @@ + @@ -71,12 +72,6 @@ $(DalamudLibPath)Lumina.Excel.dll false - - ..\..\..\..\..\AppData\Roaming\XIVLauncher\installedPlugins\Penumbra\0.5.0.5\Penumbra.GameData.dll - - - ..\..\..\..\..\AppData\Roaming\XIVLauncher\installedPlugins\Penumbra\0.5.0.5\Penumbra.PlayerWatch.dll - diff --git a/MareSynchronos/Models/CharacterCache.cs b/MareSynchronos/Models/CharacterCache.cs new file mode 100644 index 0000000..441b2fc --- /dev/null +++ b/MareSynchronos/Models/CharacterCache.cs @@ -0,0 +1,89 @@ +using Dalamud.Logging; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MareSynchronos.Models +{ + [JsonObject(MemberSerialization.OptIn)] + public class CharacterCache + { + public List FileReplacements { get; set; } = new List(); + + [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)) + .ToList(); + + public CharacterCache() + { + + } + + public void Invalidate(List? fileReplacements = null) + { + try + { + var fileReplacement = fileReplacements ?? FileReplacements.ToList(); + foreach (var item in fileReplacement) + { + item.IsInUse = false; + Invalidate(item.Associated); + if (FileReplacements.Contains(item)) + { + FileReplacements.Remove(item); + } + } + } + catch (Exception ex) + { + PluginLog.Debug(ex.Message); + } + } + + public void AddAssociatedResource(FileReplacement resource, FileReplacement mdlParent, FileReplacement mtrlParent) + { + try + { + if (resource == null) return; + if (mdlParent == null) + { + resource.IsInUse = true; + FileReplacements.Add(resource); + return; + } + + FileReplacement replacement; + + if (mtrlParent == null && (replacement = FileReplacements.SingleOrDefault(f => f == mdlParent)!) != null) + { + replacement.AddAssociated(resource); + } + + if ((replacement = FileReplacements.SingleOrDefault(f => f == mdlParent)?.Associated.SingleOrDefault(f => f == mtrlParent)!) != null) + { + replacement.AddAssociated(resource); + } + } + catch (Exception ex) + { + PluginLog.Debug(ex.Message); + } + } + + public override string ToString() + { + StringBuilder stringBuilder = new StringBuilder(); + foreach (var fileReplacement in FileReplacements.OrderBy(a => a.GamePath)) + { + stringBuilder.AppendLine(fileReplacement.ToString()); + } + return stringBuilder.ToString(); + } + } +} diff --git a/MareSynchronos/Models/FileReplacement.cs b/MareSynchronos/Models/FileReplacement.cs index 9f762f9..9ca9ed1 100644 --- a/MareSynchronos/Models/FileReplacement.cs +++ b/MareSynchronos/Models/FileReplacement.cs @@ -1,21 +1,29 @@ -using System; +using Dalamud.Logging; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json; +using MareSynchronos.FileCacheDB; namespace MareSynchronos.Models { + [JsonObject(MemberSerialization.OptIn)] public class FileReplacement { private readonly string penumbraDirectory; + [JsonProperty] public string GamePath { get; private set; } - public string ReplacedPath { get; private set; } = string.Empty; - + public string ResolvedPath { get; private set; } = string.Empty; + [JsonProperty] + public string Hash { get; set; } = string.Empty; + public bool IsInUse { get; set; } = false; public List Associated { get; set; } = new List(); - - public bool HasFileReplacement => GamePath != ReplacedPath; + [JsonProperty] + public string ImcData { get; set; } = string.Empty; + public bool HasFileReplacement => GamePath != ResolvedPath; public FileReplacement(string gamePath, string penumbraDirectory) { GamePath = gamePath; @@ -24,6 +32,8 @@ namespace MareSynchronos.Models public void AddAssociated(FileReplacement fileReplacement) { + fileReplacement.IsInUse = true; + if (!Associated.Any(a => a.IsReplacedByThis(fileReplacement))) { Associated.Add(fileReplacement); @@ -35,31 +45,40 @@ namespace MareSynchronos.Models GamePath = path; } - public void SetReplacedPath(string path) + public void SetResolvedPath(string path) { - ReplacedPath = path.ToLower().Replace('/', '\\').Replace(penumbraDirectory, "").Replace('\\', '/'); + ResolvedPath = path.ToLower().Replace('/', '\\').Replace(penumbraDirectory, "").Replace('\\', '/'); + if (!HasFileReplacement) return; + + Task.Run(() => + { + using FileCacheContext db = new FileCacheContext(); + var fileCache = db.FileCaches.SingleOrDefault(f => f.Filepath == path.ToLower()); + if (fileCache != null) + Hash = fileCache.Hash; + }); } public bool IsReplacedByThis(string path) { - return GamePath.ToLower() == path.ToLower() || ReplacedPath.ToLower() == path.ToLower(); + return GamePath.ToLower() == path.ToLower() || ResolvedPath.ToLower() == path.ToLower(); } public bool IsReplacedByThis(FileReplacement replacement) { - return IsReplacedByThis(replacement.GamePath) || IsReplacedByThis(replacement.ReplacedPath); + return IsReplacedByThis(replacement.GamePath) || IsReplacedByThis(replacement.ResolvedPath); } public override string ToString() { StringBuilder builder = new StringBuilder(); - builder.AppendLine($"Modded: {HasFileReplacement} - {GamePath} => {ReplacedPath}"); + builder.AppendLine($"Modded: {HasFileReplacement} - {GamePath} => {ResolvedPath}"); foreach (var l1 in Associated) { - builder.AppendLine($" + Modded: {l1.HasFileReplacement} - {l1.GamePath} => {l1.ReplacedPath}"); + builder.AppendLine($" + Modded: {l1.HasFileReplacement} - {l1.GamePath} => {l1.ResolvedPath}"); foreach (var l2 in l1.Associated) { - builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {l2.GamePath} => {l2.ReplacedPath}"); + builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {l2.GamePath} => {l2.ResolvedPath}"); } } return builder.ToString(); diff --git a/MareSynchronos/PenumbraMod/DefaultMod.cs b/MareSynchronos/PenumbraMod/DefaultMod.cs new file mode 100644 index 0000000..6cf02ee --- /dev/null +++ b/MareSynchronos/PenumbraMod/DefaultMod.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MareSynchronos.PenumbraMod +{ + [JsonObject(MemberSerialization.OptOut)] + internal class DefaultMod + { + public string Name { get; set; } = "Default"; + public int Priority { get; set; } = 0; + public Dictionary Files { get; set; } = new(); + public Dictionary FileSwaps { get; set; } = new(); + public List Manipulations { get; set; } = new(); + } +} diff --git a/MareSynchronos/PenumbraMod/Meta.cs b/MareSynchronos/PenumbraMod/Meta.cs new file mode 100644 index 0000000..1f9ebe5 --- /dev/null +++ b/MareSynchronos/PenumbraMod/Meta.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MareSynchronos.PenumbraMod +{ + [JsonObject(MemberSerialization.OptOut)] + internal class Meta + { + public int FileVersion { get; set; } = 1; + public string Name { get; set; } = string.Empty; + public string Author { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string Version { get; set; } = "0"; + public string Website { get; set; } = string.Empty; + public long ImportDate { get; set; } = DateTime.Now.Ticks; + } +} diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index b038bde..bc40ebc 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -23,6 +23,14 @@ using System.Text; using Penumbra.GameData.Enums; using System; using MareSynchronos.Models; +using Dalamud.Game.Gui; +using MareSynchronos.PenumbraMod; +using System.Text.Json; +using System.Text.Encodings.Web; +using System.Text.Unicode; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System.Reflection; namespace SamplePlugin { @@ -30,7 +38,8 @@ namespace SamplePlugin { public string Name => "Mare Synchronos"; - private const string commandName = "/pscan"; + private const string commandName = "/mare"; + private readonly ClientState clientState; private DalamudPluginInterface PluginInterface { get; init; } private CommandManager CommandManager { get; init; } @@ -45,11 +54,11 @@ namespace SamplePlugin public Plugin( [RequiredVersion("1.0")] DalamudPluginInterface pluginInterface, [RequiredVersion("1.0")] CommandManager commandManager, - Framework framework, ObjectTable objectTable, ClientState clientState, DataManager dataManager) + Framework framework, ObjectTable objectTable, ClientState clientState, DataManager dataManager, GameGui gameGui) { this.PluginInterface = pluginInterface; this.CommandManager = commandManager; - + this.clientState = clientState; this.Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); this.Configuration.Initialize(this.PluginInterface); @@ -67,7 +76,7 @@ namespace SamplePlugin this.PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI; playerWatch = PlayerWatchFactory.Create(framework, clientState, objectTable); - drawHooks = new DrawHooks(pluginInterface, clientState, objectTable, new MareSynchronos.Models.FileReplacementFactory(pluginInterface, clientState)); + drawHooks = new DrawHooks(pluginInterface, clientState, objectTable, new FileReplacementFactory(pluginInterface, clientState), gameGui); } public void Dispose() @@ -87,6 +96,17 @@ namespace SamplePlugin return; } + if(args == "playerdata") + { + PluginLog.Debug(PluginInterface.GetIpcSubscriber("Glamourer.GetCharacterCustomization").InvokeFunc()); + } + + if(args == "applyglam") + { + PluginInterface.GetIpcSubscriber("Glamourer.ApplyCharacterCustomization") + .InvokeAction("Ah3/DwQBAR4IBHOABIOceTIIApkDAgADQmQBZJqepQZlAAEAAAAAAAAAAACcEwEAyxcBbrAXAUnKFwJIuBcBBkYAAQBIAAEANQABADUAAQACAAQAAQAAAIA/Eg==", "Ilya Zhelmo"); + } + if (args == "scan") { cts = new CancellationTokenSource(); @@ -116,45 +136,81 @@ namespace SamplePlugin var resources = drawHooks.PrintRequestedResources(); } - if (args == "copy") + if (args == "printjson") + { + var cache = drawHooks.BuildCharacterCache(); + var json = JsonConvert.SerializeObject(cache, Formatting.Indented); + PluginLog.Debug(json); + } + + if (args == "createtestmod") { - var resources = drawHooks.PrintRequestedResources(); Task.Run(() => { - PluginLog.Debug("Copying files"); - foreach (var file in Directory.GetFiles(@"G:\Penumbra\TestMod\files")) + var playerName = clientState.LocalPlayer!.Name.ToString(); + var modName = $"Mare Synchronos Test Mod {playerName}"; + var modDirectory = PluginInterface.GetIpcSubscriber("Penumbra.GetModDirectory").InvokeFunc(); + string modDirectoryPath = Path.Combine(modDirectory, modName); + if (Directory.Exists(modDirectoryPath)) { - File.Delete(file); + Directory.Delete(modDirectoryPath, true); } - File.Delete(@"G:\Penumbra\testmod\filelist.txt"); - using FileCacheContext db = new FileCacheContext(); + + Directory.CreateDirectory(modDirectoryPath); + Directory.CreateDirectory(Path.Combine(modDirectoryPath, "files")); + Meta meta = new Meta() + { + Name = modName, + Author = playerName, + Description = "Mare Synchronous Test Mod Export", + }; + + var resources = drawHooks.PrintRequestedResources(); + var metaJson = JsonConvert.SerializeObject(meta); + File.WriteAllText(Path.Combine(modDirectoryPath, "meta.json"), metaJson); + + DefaultMod defaultMod = new DefaultMod(); + + using var db = new FileCacheContext(); foreach (var resource in resources) { - CopyRecursive(resource, db); + CopyRecursive(resource, modDirectoryPath, db, defaultMod.Files); } + + var defaultModJson = JsonConvert.SerializeObject(defaultMod); + File.WriteAllText(Path.Combine(modDirectoryPath, "default_mod.json"), defaultModJson); + + PluginLog.Debug("Mod created to " + modDirectoryPath); }); } } - private void CopyRecursive(FileReplacement replacement, FileCacheContext db) + private void CopyRecursive(FileReplacement replacement, string targetDirectory, FileCacheContext db, Dictionary? resourceDict = null) { if (replacement.HasFileReplacement) { - PluginLog.Debug("Copying file \"" + replacement.ReplacedPath + "\""); + PluginLog.Debug("Copying file \"" + replacement.ResolvedPath + "\""); - var fileCache = db.FileCaches.Single(f => f.Filepath.Contains(replacement.ReplacedPath.Replace('/', '\\'))); + var fileCache = db.FileCaches.Single(f => f.Filepath.Contains(replacement.ResolvedPath.Replace('/', '\\'))); try { var ext = new FileInfo(fileCache.Filepath).Extension; - File.Copy(fileCache.Filepath, Path.Combine(@"G:\Penumbra\TestMod\files", fileCache.Hash.ToLower() + ext)); - File.AppendAllLines(Path.Combine(@"G:\Penumbra\TestMod", "filelist.txt"), new[] { $"\"{replacement.GamePath}\": \"files\\\\{fileCache.Hash.ToLower() + ext}\"," }); + File.Copy(fileCache.Filepath, Path.Combine(targetDirectory, "files", fileCache.Hash.ToLower() + ext)); + if (resourceDict != null) + { + resourceDict[replacement.GamePath] = $"files\\{fileCache.Hash.ToLower() + ext}"; + } + else + { + File.AppendAllLines(Path.Combine(targetDirectory, "filelist.txt"), new[] { $"\"{replacement.GamePath}\": \"files\\\\{fileCache.Hash.ToLower() + ext}\"," }); + } } catch { } } foreach (var associated in replacement.Associated) { - CopyRecursive(associated, db); + CopyRecursive(associated, targetDirectory, db, resourceDict); } } @@ -174,7 +230,7 @@ namespace SamplePlugin { Stopwatch st = Stopwatch.StartNew(); - string penumbraDir = Configuration.PenumbraFolder; + string penumbraDir = PluginInterface.GetIpcSubscriber("Penumbra.GetModDirectory").InvokeFunc(); PluginLog.Debug("Getting files from " + penumbraDir); ConcurrentDictionary charaFiles = new ConcurrentDictionary( Directory.GetFiles(penumbraDir, "*.*", SearchOption.AllDirectories)