From b6b00f21e209597224fce5701d2dcb3ae1b75db5 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Mon, 13 Jun 2022 13:05:05 +0200 Subject: [PATCH] add spaghetti --- MareSynchronos.sln | 26 + MareSynchronos/Hooks/DrawHooks.cs | 453 ++++++++++++++++++ MareSynchronos/Interop/Material.cs | 30 ++ MareSynchronos/Interop/MtrlResource.cs | 28 ++ MareSynchronos/Interop/RenderModel.cs | 41 ++ MareSynchronos/Interop/ResourceHandle.cs | 96 ++++ MareSynchronos/MareSynchronos.csproj | 14 + MareSynchronos/Models/FileReplacement.cs | 68 +++ .../Models/FileReplacementFactory.cs | 39 ++ MareSynchronos/Plugin.cs | 156 +++++- 10 files changed, 950 insertions(+), 1 deletion(-) create mode 100644 MareSynchronos/Hooks/DrawHooks.cs create mode 100644 MareSynchronos/Interop/Material.cs create mode 100644 MareSynchronos/Interop/MtrlResource.cs create mode 100644 MareSynchronos/Interop/RenderModel.cs create mode 100644 MareSynchronos/Interop/ResourceHandle.cs create mode 100644 MareSynchronos/Models/FileReplacement.cs create mode 100644 MareSynchronos/Models/FileReplacementFactory.cs diff --git a/MareSynchronos.sln b/MareSynchronos.sln index 656c710..40637a5 100644 --- a/MareSynchronos.sln +++ b/MareSynchronos.sln @@ -5,16 +5,42 @@ VisualStudioVersion = 17.1.32328.378 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos", "MareSynchronos\MareSynchronos.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "..\..\Penumbra\Penumbra.GameData\Penumbra.GameData.csproj", "{44F7CA6A-898C-4901-ADB8-010BC74FF781}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.PlayerWatch", "..\..\Penumbra\Penumbra.PlayerWatch\Penumbra.PlayerWatch.csproj", "{2F26FC2D-03DF-445F-A87B-8708D621E86C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 + {44F7CA6A-898C-4901-ADB8-010BC74FF781}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44F7CA6A-898C-4901-ADB8-010BC74FF781}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44F7CA6A-898C-4901-ADB8-010BC74FF781}.Debug|x64.ActiveCfg = Debug|Any CPU + {44F7CA6A-898C-4901-ADB8-010BC74FF781}.Debug|x64.Build.0 = Debug|Any CPU + {44F7CA6A-898C-4901-ADB8-010BC74FF781}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44F7CA6A-898C-4901-ADB8-010BC74FF781}.Release|Any CPU.Build.0 = Release|Any CPU + {44F7CA6A-898C-4901-ADB8-010BC74FF781}.Release|x64.ActiveCfg = Release|Any CPU + {44F7CA6A-898C-4901-ADB8-010BC74FF781}.Release|x64.Build.0 = Release|Any CPU + {2F26FC2D-03DF-445F-A87B-8708D621E86C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F26FC2D-03DF-445F-A87B-8708D621E86C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F26FC2D-03DF-445F-A87B-8708D621E86C}.Debug|x64.ActiveCfg = Debug|Any CPU + {2F26FC2D-03DF-445F-A87B-8708D621E86C}.Debug|x64.Build.0 = Debug|Any CPU + {2F26FC2D-03DF-445F-A87B-8708D621E86C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F26FC2D-03DF-445F-A87B-8708D621E86C}.Release|Any CPU.Build.0 = Release|Any CPU + {2F26FC2D-03DF-445F-A87B-8708D621E86C}.Release|x64.ActiveCfg = Release|Any CPU + {2F26FC2D-03DF-445F-A87B-8708D621E86C}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MareSynchronos/Hooks/DrawHooks.cs b/MareSynchronos/Hooks/DrawHooks.cs new file mode 100644 index 0000000..67c52c1 --- /dev/null +++ b/MareSynchronos/Hooks/DrawHooks.cs @@ -0,0 +1,453 @@ +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +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 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.Text; +using System.Threading.Tasks; + +namespace MareSynchronos.Hooks +{ + public unsafe class DrawHooks : IDisposable + { + [Signature("48 8D 05 ?? ?? ?? ?? 48 89 03 48 8D 8B ?? ?? ?? ?? 44 89 83 ?? ?? ?? ?? 48 8B C1", ScanType = ScanType.StaticAddress)] + public IntPtr* DrawObjectHumanVTable; + + // [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; + + public DrawHooks(DalamudPluginInterface pluginInterface, ClientState clientState, ObjectTable objectTable, FileReplacementFactory factory) + { + this.pluginInterface = pluginInterface; + this.clientState = clientState; + this.objectTable = objectTable; + this.factory = factory; + SignatureHelper.Initialise(this); + } + + public void StartHooks() + { + allRequestedResources.Clear(); + SetupHumanHooks(); + EnableHumanHooks(); + PluginLog.Debug("Hooks enabled"); + } + + public void StopHooks() + { + DisableHumanHooks(); + DisposeHumanHooks(); + } + + private void SetupHumanHooks() + { + 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); + } + + private void EnableHumanHooks() + { + if (ResolveDecalPathHook?.IsEnabled ?? false) return; + + 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(); + } + + private void DisableHumanHooks() + { + ResolveDecalPathHook?.Disable(); + //ResolveEidPathHook?.Disable(); + //ResolveImcPathHook?.Disable(); + //ResolveMPapPathHook?.Disable(); + ResolveMdlPathHook?.Disable(); + ResolveMtrlPathHook?.Disable(); + //ResolvePapPathHook?.Disable(); + //ResolvePhybPathHook?.Disable(); + //ResolveSklbPathHook?.Disable(); + //ResolveSkpPathHook?.Disable(); + //ResolveTmbPathHook?.Disable(); + //ResolveVfxPathHook?.Disable(); + EnableDrawHook?.Disable(); + LoadMtrlTexHook?.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(); + } + + // 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; + EnableDrawHook!.Original.Invoke(gameObject, b, c, d); + LastGameObject = oldObject; + //PluginLog.Debug("Draw end"); + } + + 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) + { + LoadMtrlHelper(mtrlResourceHandle); + var ret = LoadMtrlTexHook!.Original(mtrlResourceHandle); + return ret; + } + + 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); + var mtrlResource = factory.Create(mtrlPath.ToString()); + var existingMat = loadedMaterials.FirstOrDefault(m => m.IsReplacedByThis(mtrlResource)); + if (existingMat != null) + { + for (int i = 0; i < mtrl->NumTex; i++) + { + var texPath = new Utf8String(mtrl->TexString(i)); + AddRequestedResource(factory.Create(texPath.ToString())); + } + + loadedMaterials.Remove(existingMat); + } + } + catch (Exception ex) + { + PluginLog.Error(ex, "error"); + } + } + + private unsafe IntPtr ResolvePathDetour(IntPtr drawObject, IntPtr path) + { + if (path == IntPtr.Zero) + { + return path; + } + + var gamepath = new Utf8String((byte*)path); + + var playerName = clientState.LocalPlayer.Name.ToString(); + var gameDrawObject = (DrawObject*)drawObject; + var playerDrawObject = ((Character*)clientState.LocalPlayer.Address)->GameObject.GetDrawObject(); + + if (LastGameObject != null && (LastGameObject->DrawObject == null || LastGameObject->DrawObject == gameDrawObject)) + { + var owner = new Utf8String(LastGameObject->Name).ToString(); + if (owner != 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); + } + + AddRequestedResource(resource); + } + + return path; + } + + List loadedMaterials = new(); + ConcurrentBag allRequestedResources = new(); + + public List PrintRequestedResources() + { + foreach (var resource in allRequestedResources) + { + PluginLog.Debug(resource.ToString()); + } + //PluginLog.Debug("---"); + + 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(); + } + } +} diff --git a/MareSynchronos/Interop/Material.cs b/MareSynchronos/Interop/Material.cs new file mode 100644 index 0000000..963b569 --- /dev/null +++ b/MareSynchronos/Interop/Material.cs @@ -0,0 +1,30 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct Material +{ + [FieldOffset( 0x10 )] + public ResourceHandle* ResourceHandle; + + [FieldOffset( 0x28 )] + public MaterialData* MaterialData; + + [FieldOffset( 0x48 )] + public Texture* Tex1; + + [FieldOffset( 0x60 )] + public Texture* Tex2; + + [FieldOffset( 0x78 )] + public Texture* Tex3; +} + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct MaterialData +{ + [FieldOffset( 0x0 )] + public byte* Data; +} \ No newline at end of file diff --git a/MareSynchronos/Interop/MtrlResource.cs b/MareSynchronos/Interop/MtrlResource.cs new file mode 100644 index 0000000..ff5b6ab --- /dev/null +++ b/MareSynchronos/Interop/MtrlResource.cs @@ -0,0 +1,28 @@ +using System.Runtime.InteropServices; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct MtrlResource +{ + [FieldOffset( 0x00 )] + public ResourceHandle Handle; + + [FieldOffset( 0xD0 )] + public ushort* TexSpace; // Contains the offsets for the tex files inside the string list. + + [FieldOffset( 0xE0 )] + public byte* StringList; + + [FieldOffset( 0xF8 )] + public ushort ShpkOffset; + + [FieldOffset( 0xFA )] + public byte NumTex; + + public byte* ShpkString + => StringList + ShpkOffset; + + public byte* TexString( int idx ) + => StringList + *( TexSpace + 4 + idx * 8 ); +} \ No newline at end of file diff --git a/MareSynchronos/Interop/RenderModel.cs b/MareSynchronos/Interop/RenderModel.cs new file mode 100644 index 0000000..b9e0490 --- /dev/null +++ b/MareSynchronos/Interop/RenderModel.cs @@ -0,0 +1,41 @@ +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.Graphics.Render; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct RenderModel +{ + [FieldOffset( 0x18 )] + public RenderModel* PreviousModel; + + [FieldOffset( 0x20 )] + public RenderModel* NextModel; + + [FieldOffset( 0x30 )] + public ResourceHandle* ResourceHandle; + + [FieldOffset( 0x40 )] + public Skeleton* Skeleton; + + [FieldOffset( 0x58 )] + public void** BoneList; + + [FieldOffset( 0x60 )] + public int BoneListCount; + + [FieldOffset( 0x68 )] + private void* UnkDXBuffer1; + + [FieldOffset( 0x70 )] + private void* UnkDXBuffer2; + + [FieldOffset( 0x78 )] + private void* UnkDXBuffer3; + + [FieldOffset( 0x90 )] + public void** Materials; + + [FieldOffset( 0x98 )] + public int MaterialCount; +} \ No newline at end of file diff --git a/MareSynchronos/Interop/ResourceHandle.cs b/MareSynchronos/Interop/ResourceHandle.cs new file mode 100644 index 0000000..6b3a8a7 --- /dev/null +++ b/MareSynchronos/Interop/ResourceHandle.cs @@ -0,0 +1,96 @@ +using System; +using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Client.System.Resource; +using Penumbra.GameData.Enums; + +namespace Penumbra.Interop.Structs; + +[StructLayout( LayoutKind.Explicit )] +public unsafe struct ResourceHandle +{ + [StructLayout( LayoutKind.Explicit )] + public struct DataIndirection + { + [FieldOffset( 0x00 )] + public void** VTable; + + [FieldOffset( 0x10 )] + public byte* DataPtr; + + [FieldOffset( 0x28 )] + public ulong DataLength; + } + + public const int SsoSize = 15; + + public byte* FileName() + { + if( FileNameLength > SsoSize ) + { + return FileNameData; + } + + fixed( byte** name = &FileNameData ) + { + return ( byte* )name; + } + } + + public ReadOnlySpan< byte > FileNameSpan() + => new(FileName(), FileNameLength); + + [FieldOffset( 0x00 )] + public void** VTable; + + [FieldOffset( 0x08 )] + public ResourceCategory Category; + + [FieldOffset( 0x0C )] + public ResourceType FileType; + + [FieldOffset( 0x10 )] + public uint Id; + + [FieldOffset( 0x48 )] + public byte* FileNameData; + + [FieldOffset( 0x58 )] + public int FileNameLength; + + [FieldOffset( 0xAC )] + public uint RefCount; + + // May return null. + public static byte* GetData( ResourceHandle* handle ) + => ( ( delegate* unmanaged< ResourceHandle*, byte* > )handle->VTable[ 23 ] )( handle ); + + public static ulong GetLength( ResourceHandle* handle ) + => ( ( delegate* unmanaged< ResourceHandle*, ulong > )handle->VTable[ 17 ] )( handle ); + + + // Only use these if you know what you are doing. + // Those are actually only sure to be accessible for DefaultResourceHandles. + [FieldOffset( 0xB0 )] + public DataIndirection* Data; + + [FieldOffset( 0xB8 )] + public uint DataLength; + + public (IntPtr Data, int Length) GetData() + => Data != null + ? ( ( IntPtr )Data->DataPtr, ( int )Data->DataLength ) + : ( IntPtr.Zero, 0 ); + + public bool SetData( IntPtr data, int length ) + { + if( Data == null ) + { + return false; + } + + Data->DataPtr = length != 0 ? ( byte* )data : null; + Data->DataLength = ( ulong )length; + DataLength = ( uint )length; + return true; + } +} \ No newline at end of file diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index 675cea6..31eb24f 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -34,11 +34,19 @@ + + + + + $(DalamudLibPath)FFXIVClientStructs.dll false + + ..\..\..\..\..\AppData\Roaming\XIVLauncher\installedPlugins\Glamourer\0.0.9.0\Glamourer.GameData.dll + $(DalamudLibPath)Newtonsoft.Json.dll false @@ -63,6 +71,12 @@ $(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/FileReplacement.cs b/MareSynchronos/Models/FileReplacement.cs new file mode 100644 index 0000000..9f762f9 --- /dev/null +++ b/MareSynchronos/Models/FileReplacement.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MareSynchronos.Models +{ + public class FileReplacement + { + private readonly string penumbraDirectory; + + public string GamePath { get; private set; } + public string ReplacedPath { get; private set; } = string.Empty; + + public List Associated { get; set; } = new List(); + + public bool HasFileReplacement => GamePath != ReplacedPath; + public FileReplacement(string gamePath, string penumbraDirectory) + { + GamePath = gamePath; + this.penumbraDirectory = penumbraDirectory; + } + + public void AddAssociated(FileReplacement fileReplacement) + { + if (!Associated.Any(a => a.IsReplacedByThis(fileReplacement))) + { + Associated.Add(fileReplacement); + } + } + + public void SetGamePath(string path) + { + GamePath = path; + } + + public void SetReplacedPath(string path) + { + ReplacedPath = path.ToLower().Replace('/', '\\').Replace(penumbraDirectory, "").Replace('\\', '/'); + } + + public bool IsReplacedByThis(string path) + { + return GamePath.ToLower() == path.ToLower() || ReplacedPath.ToLower() == path.ToLower(); + } + + public bool IsReplacedByThis(FileReplacement replacement) + { + return IsReplacedByThis(replacement.GamePath) || IsReplacedByThis(replacement.ReplacedPath); + } + + public override string ToString() + { + StringBuilder builder = new StringBuilder(); + builder.AppendLine($"Modded: {HasFileReplacement} - {GamePath} => {ReplacedPath}"); + foreach (var l1 in Associated) + { + builder.AppendLine($" + Modded: {l1.HasFileReplacement} - {l1.GamePath} => {l1.ReplacedPath}"); + foreach (var l2 in l1.Associated) + { + builder.AppendLine($" + Modded: {l2.HasFileReplacement} - {l2.GamePath} => {l2.ReplacedPath}"); + } + } + return builder.ToString(); + } + } +} diff --git a/MareSynchronos/Models/FileReplacementFactory.cs b/MareSynchronos/Models/FileReplacementFactory.cs new file mode 100644 index 0000000..1e7e128 --- /dev/null +++ b/MareSynchronos/Models/FileReplacementFactory.cs @@ -0,0 +1,39 @@ +using Dalamud.Game.ClientState; +using Dalamud.Plugin; +using Dalamud.Plugin.Ipc; + +namespace MareSynchronos.Models +{ + public class FileReplacementFactory + { + private readonly ClientState clientState; + private ICallGateSubscriber resolvePath; + private string penumbraDirectory; + + public FileReplacementFactory(DalamudPluginInterface pluginInterface, ClientState clientState) + { + resolvePath = pluginInterface.GetIpcSubscriber("Penumbra.ResolveCharacterPath"); + penumbraDirectory = pluginInterface.GetIpcSubscriber("Penumbra.GetModDirectory").InvokeFunc().ToLower() + '\\'; + this.clientState = clientState; + } + public FileReplacement Create(string gamePath) + { + var fileReplacement = new FileReplacement(gamePath, penumbraDirectory); + fileReplacement.SetReplacedPath(resolvePath.InvokeFunc(gamePath, clientState.LocalPlayer!.Name.ToString())); + if (!fileReplacement.HasFileReplacement) + { + // try to resolve path with -- 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.SetGamePath(newTempGamePath); + } + } + return fileReplacement; + } + } +} diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index d7366a6..5bdca72 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -11,6 +11,18 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MareSynchronos.Hooks; +using Penumbra.PlayerWatch; +using Dalamud.Game; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState; +using Dalamud.Data; +using Lumina.Excel.GeneratedSheets; +using Glamourer.Customization; +using System.Text; +using Penumbra.GameData.Enums; +using System; +using MareSynchronos.Models; namespace SamplePlugin { @@ -25,12 +37,15 @@ namespace SamplePlugin private Configuration Configuration { get; init; } private PluginUI PluginUi { get; init; } private FileCacheFactory FileCacheFactory { get; init; } + private DrawHooks drawHooks; private CancellationTokenSource cts; + private IPlayerWatcher playerWatch; public Plugin( [RequiredVersion("1.0")] DalamudPluginInterface pluginInterface, - [RequiredVersion("1.0")] CommandManager commandManager) + [RequiredVersion("1.0")] CommandManager commandManager, + Framework framework, ObjectTable objectTable, ClientState clientState, DataManager dataManager) { this.PluginInterface = pluginInterface; this.CommandManager = commandManager; @@ -50,12 +65,18 @@ namespace SamplePlugin this.PluginInterface.UiBuilder.Draw += DrawUI; this.PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI; + + playerWatch = PlayerWatchFactory.Create(framework, clientState, objectTable); + drawHooks = new DrawHooks(pluginInterface, clientState, objectTable, new MareSynchronos.Models.FileReplacementFactory(pluginInterface, clientState)); } public void Dispose() { this.PluginUi.Dispose(); this.CommandManager.RemoveHandler(commandName); + playerWatch.PlayerChanged -= PlayerWatch_PlayerChanged; + playerWatch.RemovePlayerFromWatch("Ilya Zhelmo"); + drawHooks.Dispose(); } private void OnCommand(string command, string args) @@ -72,6 +93,139 @@ namespace SamplePlugin Task.Run(() => StartScan(), cts.Token); } + + if (args == "watch") + { + playerWatch.AddPlayerToWatch("Ilya Zhelmo"); + playerWatch.PlayerChanged += PlayerWatch_PlayerChanged; + } + + if (args == "stopwatch") + { + playerWatch.PlayerChanged -= PlayerWatch_PlayerChanged; + playerWatch.RemovePlayerFromWatch("Ilya Zhelmo"); + } + + if (args == "hook") + { + drawHooks.StartHooks(); + } + + if (args == "print") + { + var resources = drawHooks.PrintRequestedResources(); + } + + if (args == "copy") + { + var resources = drawHooks.PrintRequestedResources(); + Task.Run(() => + { + PluginLog.Debug("Copying files"); + foreach (var file in Directory.GetFiles(@"G:\Penumbra\TestMod\files")) + { + File.Delete(file); + } + File.Delete(@"G:\Penumbra\testmod\filelist.txt"); + using FileCacheContext db = new FileCacheContext(); + foreach (var resource in resources) + { + CopyRecursive(resource, db); + } + }); + } + } + + private void CopyRecursive(FileReplacement replacement, FileCacheContext db) + { + if (replacement.HasFileReplacement) + { + PluginLog.Debug("Copying file \"" + replacement.ReplacedPath + "\""); + + var fileCache = db.FileCaches.Single(f => f.Filepath.Contains(replacement.ReplacedPath.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}\"," }); + } + catch { } + } + + foreach (var associated in replacement.Associated) + { + CopyRecursive(associated, db); + } + } + + private void PlayerWatch_PlayerChanged(Dalamud.Game.ClientState.Objects.Types.Character actor) + { + var equipment = playerWatch.UpdatePlayerWithoutEvent(actor); + var customization = new CharacterCustomization(actor); + DebugCustomization(customization); + //PluginLog.Debug(customization.Gender.ToString()); + if (equipment != null) + { + PluginLog.Debug(equipment.ToString()); + } + } + + private void DebugCustomization(CharacterCustomization customization) + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Gender: " + customization[CustomizationId.Gender].ToString() + ":" + customization.Gender.ToName()); + sb.AppendLine("Race: " + customization[CustomizationId.Race].ToString() + ":" + GetBodyRaceCode(customization)); + sb.AppendLine("Face: " + customization.Face.ToString()); + + PluginLog.Debug(sb.ToString()); + } + + private string GetBodyRaceMdlPath(CharacterCustomization customization) + { + return ""; + } + + private string GetBodyRaceCode(CharacterCustomization customization) + { + return Names.CombinedRace(customization.Gender, GetBodyRace(FromSubRace(customization.Clan))).ToRaceCode(); + } + + private ModelRace GetBodyRace(ModelRace modelRace) + { + return modelRace switch + { + ModelRace.AuRa => ModelRace.AuRa, + ModelRace.Miqote => ModelRace.Midlander, + ModelRace.Highlander => ModelRace.Highlander, + ModelRace.Lalafell => ModelRace.Lalafell, + ModelRace.Midlander => ModelRace.Midlander, + ModelRace.Elezen => ModelRace.Midlander, + ModelRace.Hrothgar => ModelRace.Hrothgar, + }; + } + + private ModelRace FromSubRace(SubRace race) + { + return race switch + { + SubRace.Xaela => ModelRace.AuRa, + SubRace.Raen => ModelRace.AuRa, + SubRace.Highlander => ModelRace.Highlander, + SubRace.Midlander => ModelRace.Midlander, + SubRace.Plainsfolk => ModelRace.Lalafell, + SubRace.Dunesfolk => ModelRace.Lalafell, + SubRace.SeekerOfTheSun => ModelRace.Miqote, + SubRace.KeeperOfTheMoon => ModelRace.Miqote, + SubRace.Seawolf => ModelRace.Roegadyn, + SubRace.Hellsguard => ModelRace.Roegadyn, + SubRace.Rava => ModelRace.Viera, + SubRace.Veena => ModelRace.Viera, + SubRace.Wildwood => ModelRace.Elezen, + SubRace.Duskwight => ModelRace.Elezen, + SubRace.Helion => ModelRace.Hrothgar, + SubRace.Lost => ModelRace.Hrothgar, + _ => ModelRace.Unknown + }; } private void StartScan()