remove DrawHooks, use new penumbra IPC calls (otter pls)
This commit is contained in:
		| @@ -7,45 +7,20 @@ namespace MareSynchronos.Factories | |||||||
|     public class FileReplacementFactory |     public class FileReplacementFactory | ||||||
|     { |     { | ||||||
|         private readonly IpcManager ipcManager; |         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.ipcManager = ipcManager; | ||||||
|             this.clientState = clientState; |  | ||||||
|             playerName = null!; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         public FileReplacement Create(string gamePath, bool resolve = true) |         public FileReplacement Create() | ||||||
|         { |         { | ||||||
|             if (!ipcManager.CheckPenumbraAPI()) |             if (!ipcManager.CheckPenumbraAPI()) | ||||||
|             { |             { | ||||||
|                 throw new System.Exception(); |                 throw new System.Exception(); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             var fileReplacement = new FileReplacement(gamePath, ipcManager.PenumbraModDirectory()!); |             return new FileReplacement(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; |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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<CharacterBaseCreateDelegate>? 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<CharacterBaseDestructorDelegate>? 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<EnableDrawDelegate>? EnableDrawHook; |  | ||||||
|         [Signature("4C 8B DC 49 89 5B ?? 49 89 73 ?? 55 57 41 55", DetourName = "LoadMtrlTexDetour")] |  | ||||||
|         public Hook<LoadMtrlFilesDelegate>? LoadMtrlTexHook; |  | ||||||
|  |  | ||||||
|         public event EventHandler? PlayerLoadEvent; |  | ||||||
|  |  | ||||||
|         public Hook<GeneralResolveDelegate>? ResolveMdlPathHook; |  | ||||||
|         public Hook<MaterialResolveDetour>? ResolveMtrlPathHook; |  | ||||||
|         private readonly ClientState clientState; |  | ||||||
|         private readonly Dictionary<IntPtr, ushort> DrawObjectToObject = new(); |  | ||||||
|         private readonly FileReplacementFactory factory; |  | ||||||
|         private readonly GameGui gameGui; |  | ||||||
|         private readonly ObjectTable objectTable; |  | ||||||
|         private readonly DalamudPluginInterface pluginInterface; |  | ||||||
|         private ConcurrentDictionary<string, FileReplacement> cachedResources = new(); |  | ||||||
|         private GameObject* lastGameObject = null; |  | ||||||
|         private ConcurrentBag<FileReplacement> 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<GeneralResolveDelegate>(DrawObjectHumanVTable[ResolveMdlIdx], ResolveMdlDetour); |  | ||||||
|             ResolveMtrlPathHook = new Hook<MaterialResolveDetour>(DrawObjectHumanVTable[ResolveMtrlIdx], ResolveMtrlDetour); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -4,15 +4,21 @@ using Dalamud.Game.ClientState.Objects; | |||||||
| using Dalamud.Logging; | using Dalamud.Logging; | ||||||
| using FFXIVClientStructs.FFXIV.Client.Game.Character; | using FFXIVClientStructs.FFXIV.Client.Game.Character; | ||||||
| using FFXIVClientStructs.FFXIV.Client.Game.Object; | 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.Models; | ||||||
| using MareSynchronos.Utils; | using MareSynchronos.Utils; | ||||||
| using MareSynchronos.WebAPI; | using MareSynchronos.WebAPI; | ||||||
| using Newtonsoft.Json; | using Newtonsoft.Json; | ||||||
|  | using Penumbra.GameData.ByteString; | ||||||
|  | using Penumbra.Interop.Structs; | ||||||
|  | using Penumbra.PlayerWatch; | ||||||
| using System; | using System; | ||||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||||
|  | using System.Diagnostics; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Text; | using System.Runtime.InteropServices; | ||||||
| using System.Threading; | using System.Threading; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
|  |  | ||||||
| @@ -20,26 +26,37 @@ namespace MareSynchronos.Managers | |||||||
| { | { | ||||||
|     public class CharacterManager : IDisposable |     public class CharacterManager : IDisposable | ||||||
|     { |     { | ||||||
|         private readonly DrawHooks drawHooks; |  | ||||||
|         private readonly ClientState clientState; |         private readonly ClientState clientState; | ||||||
|         private readonly Framework framework; |         private readonly Framework framework; | ||||||
|         private readonly ApiController apiController; |         private readonly ApiController apiController; | ||||||
|         private readonly ObjectTable objectTable; |         private readonly ObjectTable objectTable; | ||||||
|         private readonly IpcManager ipcManager; |         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.clientState = clientState; | ||||||
|             this.framework = framework; |             this.framework = framework; | ||||||
|             this.apiController = apiController; |             this.apiController = apiController; | ||||||
|             this.objectTable = objectTable; |             this.objectTable = objectTable; | ||||||
|             this.ipcManager = ipcManager; |             this.ipcManager = ipcManager; | ||||||
|             drawHooks.StartHooks(); |             this.factory = factory; | ||||||
|  |             watcher = PlayerWatchFactory.Create(framework, clientState, objectTable); | ||||||
|             clientState.TerritoryChanged += ClientState_TerritoryChanged; |             clientState.TerritoryChanged += ClientState_TerritoryChanged; | ||||||
|             framework.Update += Framework_Update; |             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<string, string> localPlayers = new(); |         Dictionary<string, string> 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) |         private void ClientState_TerritoryChanged(object? sender, ushort e) | ||||||
|         { |         { | ||||||
|             localPlayers.Clear(); |             localPlayers.Clear(); | ||||||
| @@ -89,46 +218,31 @@ namespace MareSynchronos.Managers | |||||||
|         public void Dispose() |         public void Dispose() | ||||||
|         { |         { | ||||||
|             framework.Update -= Framework_Update; |             framework.Update -= Framework_Update; | ||||||
|             drawHooks.PlayerLoadEvent -= Drawhooks_PlayerLoadEvent; |  | ||||||
|             clientState.TerritoryChanged -= ClientState_TerritoryChanged; |             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; |             watcher.AddPlayerToWatch(clientState.LocalPlayer!.Name.ToString()); | ||||||
|             if (drawHookTask != null && !drawHookTask.IsCompleted) return; |             watcher.PlayerChanged += Watcher_PlayerChanged; | ||||||
|  |             watcher.Enable(); | ||||||
|             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); |  | ||||||
|             }); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         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<CharacterCache> CreateFullCharacterCache() |         private async Task<CharacterCache> CreateFullCharacterCache() | ||||||
|         { |         { | ||||||
|             var cache = drawHooks.BuildCharacterCache(); |             var cache = BuildCharacterCache(); | ||||||
|             cache.SetGlamourerData(ipcManager.GlamourerGetCharacterCustomization()!); |             cache.SetGlamourerData(ipcManager.GlamourerGetCharacterCustomization()!); | ||||||
|             cache.JobId = clientState.LocalPlayer!.ClassJob.Id; |             cache.JobId = clientState.LocalPlayer!.ClassJob.Id; | ||||||
|             await Task.Run(async () => |             await Task.Run(async () => | ||||||
| @@ -137,6 +251,7 @@ namespace MareSynchronos.Managers | |||||||
|                 { |                 { | ||||||
|                     await Task.Delay(50); |                     await Task.Delay(50); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 var json = JsonConvert.SerializeObject(cache, Formatting.Indented); |                 var json = JsonConvert.SerializeObject(cache, Formatting.Indented); | ||||||
|  |  | ||||||
|                 cache.CacheHash = Crypto.GetHash(json); |                 cache.CacheHash = Crypto.GetHash(json); | ||||||
| @@ -145,12 +260,12 @@ namespace MareSynchronos.Managers | |||||||
|             return cache; |             return cache; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         public void DebugJson() |         public async Task DebugJson() | ||||||
|         { |         { | ||||||
|             var cache = CreateFullCharacterCache(); |             var cache = CreateFullCharacterCache(); | ||||||
|             while (!cache.IsCompleted) |             while (!cache.IsCompleted) | ||||||
|             { |             { | ||||||
|                 Task.Delay(50); |                 await Task.Delay(50); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             PluginLog.Debug(JsonConvert.SerializeObject(cache.Result, Formatting.Indented)); |             PluginLog.Debug(JsonConvert.SerializeObject(cache.Result, Formatting.Indented)); | ||||||
|   | |||||||
| @@ -15,10 +15,14 @@ namespace MareSynchronos.Managers | |||||||
|         private ICallGateSubscriber<string, string, object>? glamourerApplyCharacterCustomization; |         private ICallGateSubscriber<string, string, object>? glamourerApplyCharacterCustomization; | ||||||
|         private ICallGateSubscriber<int> penumbraApiVersion; |         private ICallGateSubscriber<int> penumbraApiVersion; | ||||||
|         private ICallGateSubscriber<int> glamourerApiVersion; |         private ICallGateSubscriber<int> glamourerApiVersion; | ||||||
|  |         private ICallGateSubscriber<string, string> penumbraObjectIsRedrawn; | ||||||
|         private ICallGateSubscriber<string, int, object>? penumbraRedraw; |         private ICallGateSubscriber<string, int, object>? penumbraRedraw; | ||||||
|  |         private ICallGateSubscriber<string, string, string[]>? penumbraReverseResolvePath; | ||||||
|  |  | ||||||
|         public bool Initialized { get; private set; } = false; |         public bool Initialized { get; private set; } = false; | ||||||
|  |  | ||||||
|  |         public event EventHandler? PenumbraRedrawEvent; | ||||||
|  |  | ||||||
|         public IpcManager(DalamudPluginInterface pi) |         public IpcManager(DalamudPluginInterface pi) | ||||||
|         { |         { | ||||||
|             pluginInterface = pi; |             pluginInterface = pi; | ||||||
| @@ -29,9 +33,12 @@ namespace MareSynchronos.Managers | |||||||
|             penumbraRedraw = pluginInterface.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName"); |             penumbraRedraw = pluginInterface.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName"); | ||||||
|             glamourerGetCharacterCustomization = pluginInterface.GetIpcSubscriber<string>("Glamourer.GetCharacterCustomization"); |             glamourerGetCharacterCustomization = pluginInterface.GetIpcSubscriber<string>("Glamourer.GetCharacterCustomization"); | ||||||
|             glamourerApplyCharacterCustomization = pluginInterface.GetIpcSubscriber<string, string, object>("Glamourer.ApplyCharacterCustomization"); |             glamourerApplyCharacterCustomization = pluginInterface.GetIpcSubscriber<string, string, object>("Glamourer.ApplyCharacterCustomization"); | ||||||
|  |             penumbraReverseResolvePath = pluginInterface.GetIpcSubscriber<string, string, string[]>("Penumbra.ReverseResolvePath"); | ||||||
|             penumbraApiVersion = pluginInterface.GetIpcSubscriber<int>("Penumbra.ApiVersion"); |             penumbraApiVersion = pluginInterface.GetIpcSubscriber<int>("Penumbra.ApiVersion"); | ||||||
|             glamourerApiVersion = pluginInterface.GetIpcSubscriber<int>("Glamourer.ApiVersion"); |             glamourerApiVersion = pluginInterface.GetIpcSubscriber<int>("Glamourer.ApiVersion"); | ||||||
|             penumbraInit.Subscribe(() => penumbraRedraw!.InvokeAction("self", 0)); |             penumbraObjectIsRedrawn = pluginInterface.GetIpcSubscriber<string, string>("Penumbra.ObjectIsRedrawn"); | ||||||
|  |             penumbraObjectIsRedrawn.Subscribe(RedrawEvent); | ||||||
|  |             penumbraInit.Subscribe(RedrawSelf); | ||||||
|  |  | ||||||
|             Initialized = true; |             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() |         private void Uninitialize() | ||||||
|         { |         { | ||||||
|  |             penumbraInit.Unsubscribe(RedrawSelf); | ||||||
|  |             penumbraObjectIsRedrawn.Unsubscribe(RedrawEvent); | ||||||
|             penumbraResolvePath = null; |             penumbraResolvePath = null; | ||||||
|             penumbraResolveModDir = null; |             penumbraResolveModDir = null; | ||||||
|             glamourerGetCharacterCustomization = null; |             glamourerGetCharacterCustomization = null; | ||||||
|             glamourerApplyCharacterCustomization = null; |             glamourerApplyCharacterCustomization = null; | ||||||
|  |             penumbraReverseResolvePath = null; | ||||||
|             Initialized = false; |             Initialized = false; | ||||||
|             PluginLog.Debug("IPC Manager disposed"); |             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) |         public string? PenumbraResolvePath(string path, string characterName) | ||||||
|         { |         { | ||||||
|             if (!CheckPenumbraAPI()) return null; |             if (!CheckPenumbraAPI()) return null; | ||||||
|   | |||||||
| @@ -13,10 +13,10 @@ namespace MareSynchronos.Models | |||||||
|     { |     { | ||||||
|         [JsonProperty] |         [JsonProperty] | ||||||
|         public List<FileReplacement> AllReplacements => |         public List<FileReplacement> AllReplacements => | ||||||
|             FileReplacements.Where(x => x.HasFileReplacement) |             FileReplacements.Where(f => f.HasFileReplacement) | ||||||
|             .Concat(FileReplacements.SelectMany(f => f.Associated).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)) |             .Concat(FileReplacements.SelectMany(f => f.Associated).SelectMany(f => f.Associated)).Where(f => f.HasFileReplacement) | ||||||
|             .Distinct().OrderBy(f => f.GamePath) |             .Distinct().OrderBy(f => f.GamePaths[0]) | ||||||
|             .ToList(); |             .ToList(); | ||||||
|  |  | ||||||
|         public List<FileReplacement> FileReplacements { get; set; } = new List<FileReplacement>(); |         public List<FileReplacement> FileReplacements { get; set; } = new List<FileReplacement>(); | ||||||
| @@ -31,11 +31,10 @@ namespace MareSynchronos.Models | |||||||
|  |  | ||||||
|         [JsonProperty] |         [JsonProperty] | ||||||
|         public uint JobId { get; set; } = 0; |         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 |             try | ||||||
|             { |             { | ||||||
|                 if (resource == null) return; |  | ||||||
|                 if (mdlParent == null) |                 if (mdlParent == null) | ||||||
|                 { |                 { | ||||||
|                     resource.IsInUse = true; |                     resource.IsInUse = true; | ||||||
| @@ -43,16 +42,16 @@ namespace MareSynchronos.Models | |||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 FileReplacement replacement; |                 var mdlReplacements = FileReplacements.Where(f => f == mdlParent && mtrlParent == null); | ||||||
|  |                 foreach (var mdlReplacement in mdlReplacements) | ||||||
|                 if (mtrlParent == null && (replacement = FileReplacements.SingleOrDefault(f => f == mdlParent)!) != null) |  | ||||||
|                 { |                 { | ||||||
|                     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) |             catch (Exception ex) | ||||||
| @@ -89,7 +88,7 @@ namespace MareSynchronos.Models | |||||||
|         public override string ToString() |         public override string ToString() | ||||||
|         { |         { | ||||||
|             StringBuilder stringBuilder = new(); |             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()); |                 stringBuilder.AppendLine(fileReplacement.ToString()); | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -17,21 +17,21 @@ namespace MareSynchronos.Models | |||||||
|         private readonly string penumbraDirectory; |         private readonly string penumbraDirectory; | ||||||
|  |  | ||||||
|         [JsonProperty] |         [JsonProperty] | ||||||
|         public string GamePath { get; private set; } |         public string[] GamePaths { get; set; } = Array.Empty<string>(); | ||||||
|         public string ResolvedPath { get; private set; } = string.Empty; |         [JsonProperty] | ||||||
|  |         public string ResolvedPath { get; set; } = string.Empty; | ||||||
|         [JsonProperty] |         [JsonProperty] | ||||||
|         public string Hash { get; set; } = string.Empty; |         public string Hash { get; set; } = string.Empty; | ||||||
|         public bool IsInUse { get; set; } = false; |         public bool IsInUse { get; set; } = false; | ||||||
|         public List<FileReplacement> Associated { get; set; } = new List<FileReplacement>(); |         public List<FileReplacement> Associated { get; set; } = new List<FileReplacement>(); | ||||||
|         [JsonProperty] |         [JsonProperty] | ||||||
|         public string ImcData { get; set; } = string.Empty; |         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); |         public bool Computed => (computationTask == null || (computationTask?.IsCompleted ?? true)) && Associated.All(f => f.Computed); | ||||||
|         private Task? computationTask = null; |         private Task? computationTask = null; | ||||||
|         public FileReplacement(string gamePath, string penumbraDirectory) |         public FileReplacement(string penumbraDirectory) | ||||||
|         { |         { | ||||||
|             GamePath = gamePath; |  | ||||||
|             this.penumbraDirectory = penumbraDirectory; |             this.penumbraDirectory = penumbraDirectory; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -39,15 +39,7 @@ namespace MareSynchronos.Models | |||||||
|         { |         { | ||||||
|             fileReplacement.IsInUse = true; |             fileReplacement.IsInUse = true; | ||||||
|  |  | ||||||
|             if (!Associated.Any(a => a.IsReplacedByThis(fileReplacement))) |             Associated.Add(fileReplacement); | ||||||
|             { |  | ||||||
|                 Associated.Add(fileReplacement); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         public void SetGamePath(string path) |  | ||||||
|         { |  | ||||||
|             GamePath = path; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         public void SetResolvedPath(string path) |         public void SetResolvedPath(string path) | ||||||
| @@ -107,26 +99,16 @@ namespace MareSynchronos.Models | |||||||
|             return hash; |             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() |         public override string ToString() | ||||||
|         { |         { | ||||||
|             StringBuilder builder = new(); |             StringBuilder builder = new(); | ||||||
|             builder.AppendLine($"Modded: {HasFileReplacement} - {GamePath} => {ResolvedPath}"); |             builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}"); | ||||||
|             foreach (var l1 in Associated) |             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) |                 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(); |             return builder.ToString(); | ||||||
| @@ -137,7 +119,7 @@ namespace MareSynchronos.Models | |||||||
|             if (obj == null) return true; |             if (obj == null) return true; | ||||||
|             if (obj.GetType() == typeof(FileReplacement)) |             if (obj.GetType() == typeof(FileReplacement)) | ||||||
|             { |             { | ||||||
|                 return Hash == ((FileReplacement)obj).Hash && GamePath == ((FileReplacement)obj).GamePath; |                 return Hash == ((FileReplacement)obj).Hash; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             return base.Equals(obj); |             return base.Equals(obj); | ||||||
| @@ -148,8 +130,7 @@ namespace MareSynchronos.Models | |||||||
|             int result = 13; |             int result = 13; | ||||||
|             result *= 397; |             result *= 397; | ||||||
|             result += Hash.GetHashCode(); |             result += Hash.GetHashCode(); | ||||||
|             result += GamePath.GetHashCode(); |             result += ResolvedPath.GetHashCode(); | ||||||
|             result += ImcData.GetHashCode(); |  | ||||||
|  |  | ||||||
|             return result; |             return result; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ using System.Diagnostics; | |||||||
| using System.IO; | using System.IO; | ||||||
| using System.Linq; | using System.Linq; | ||||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||||
| using MareSynchronos.Hooks; |  | ||||||
| using Dalamud.Game; | using Dalamud.Game; | ||||||
| using Dalamud.Game.ClientState.Objects; | using Dalamud.Game.ClientState.Objects; | ||||||
| using Dalamud.Game.ClientState; | using Dalamud.Game.ClientState; | ||||||
| @@ -29,21 +28,19 @@ namespace MareSynchronos | |||||||
|         private const string commandName = "/mare"; |         private const string commandName = "/mare"; | ||||||
|         private readonly ClientState clientState; |         private readonly ClientState clientState; | ||||||
|         private readonly Framework framework; |         private readonly Framework framework; | ||||||
|         private readonly GameGui gameGui; |  | ||||||
|         private readonly ObjectTable objectTable; |         private readonly ObjectTable objectTable; | ||||||
|         private readonly WindowSystem windowSystem; |         private readonly WindowSystem windowSystem; | ||||||
|         private readonly ApiController apiController; |         private readonly ApiController apiController; | ||||||
|         private CharacterManager? characterManager; |         private CharacterManager? characterManager; | ||||||
|         private IpcManager ipcManager; |         private IpcManager ipcManager; | ||||||
|         public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, |         public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, | ||||||
|             Framework framework, ObjectTable objectTable, ClientState clientState, GameGui gameGui) |             Framework framework, ObjectTable objectTable, ClientState clientState) | ||||||
|         { |         { | ||||||
|             this.PluginInterface = pluginInterface; |             this.PluginInterface = pluginInterface; | ||||||
|             this.CommandManager = commandManager; |             this.CommandManager = commandManager; | ||||||
|             this.framework = framework; |             this.framework = framework; | ||||||
|             this.objectTable = objectTable; |             this.objectTable = objectTable; | ||||||
|             this.clientState = clientState; |             this.clientState = clientState; | ||||||
|             this.gameGui = gameGui; |  | ||||||
|             Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); |             Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); | ||||||
|             Configuration.Initialize(this.PluginInterface); |             Configuration.Initialize(this.PluginInterface); | ||||||
|  |  | ||||||
| @@ -94,8 +91,8 @@ namespace MareSynchronos | |||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 characterManager = new CharacterManager( |                 characterManager = new CharacterManager( | ||||||
|                     new DrawHooks(PluginInterface, clientState, objectTable, new FileReplacementFactory(ipcManager, clientState), gameGui), |                     clientState, framework, apiController, objectTable, ipcManager, new FileReplacementFactory(ipcManager)); | ||||||
|                     clientState, framework, apiController, objectTable, ipcManager); |                 characterManager.StartWatchingPlayer(); | ||||||
|                 ipcManager.PenumbraRedraw(clientState.LocalPlayer!.Name.ToString()); |                 ipcManager.PenumbraRedraw(clientState.LocalPlayer!.Name.ToString()); | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
| @@ -140,11 +137,14 @@ namespace MareSynchronos | |||||||
|                         File.Copy(fileCache.Filepath, newFilePath); |                         File.Copy(fileCache.Filepath, newFilePath); | ||||||
|                         if (resourceDict != null) |                         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 |                         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) |         private void OnCommand(string command, string args) | ||||||
|         { |         { | ||||||
|             if (args == "print") |  | ||||||
|             { |  | ||||||
|                 characterManager?.PrintRequestedResources(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (args == "printjson") |             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") |             if (args == "createtestmod") | ||||||
| @@ -199,7 +206,7 @@ namespace MareSynchronos | |||||||
|                         Description = "Mare Synchronous Test Mod Export", |                         Description = "Mare Synchronous Test Mod Export", | ||||||
|                     }; |                     }; | ||||||
|  |  | ||||||
|                     var resources = characterManager!.GetCharacterCache(); |                     var resources = characterManager!.BuildCharacterCache(); | ||||||
|                     var metaJson = JsonConvert.SerializeObject(meta); |                     var metaJson = JsonConvert.SerializeObject(meta); | ||||||
|                     File.WriteAllText(Path.Combine(modDirectoryPath, "meta.json"), metaJson); |                     File.WriteAllText(Path.Combine(modDirectoryPath, "meta.json"), metaJson); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Stanley Dimant
					Stanley Dimant