make it resilent against restarts/reloads remove all user interaction for resource gathering compute hashes on first time file resolving and on updates of said file on resolving
421 lines
16 KiB
C#
421 lines
16 KiB
C#
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.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;
|
|
|
|
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 ConcurrentBag<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.IsInUse = false;
|
|
resource.ImcData = string.Empty;
|
|
}
|
|
|
|
PluginLog.Debug("Invaldated resource cache");
|
|
|
|
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 mdlResource = factory.Create(new Utf8String(mdl->ResourceHandle->FileName()).ToString());
|
|
var cachedMdlResource = cachedResources.First(r => r.IsReplacedByThis(mdlResource));
|
|
|
|
var imc = (ResourceHandle*)model->IMCArray[idx];
|
|
if (imc != null)
|
|
{
|
|
byte[] imcData = new byte[imc->Data->DataLength];
|
|
Marshal.Copy((IntPtr)imc->Data->DataPtr, imcData, 0, (int)imc->Data->DataLength);
|
|
string imcDataStr = BitConverter.ToString(imcData).Replace("-", "");
|
|
cachedMdlResource.ImcData = imcDataStr;
|
|
}
|
|
cache.AddAssociatedResource(cachedMdlResource, null!, null!);
|
|
|
|
for (int mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++)
|
|
{
|
|
var mtrl = (Material*)mdl->Materials[mtrlIdx];
|
|
if (mtrl == null) continue;
|
|
|
|
var mtrlFileResource = factory.Create(new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2]);
|
|
var cachedMtrlResource = cachedResources.First(r => r.IsReplacedByThis(mtrlFileResource));
|
|
cache.AddAssociatedResource(cachedMtrlResource, cachedMdlResource, null!);
|
|
|
|
var mtrlResource = (MtrlResource*)mtrl->ResourceHandle;
|
|
for (int resIdx = 0; resIdx < mtrlResource->NumTex; resIdx++)
|
|
{
|
|
var texPath = new Utf8String(mtrlResource->TexString(resIdx));
|
|
|
|
if (string.IsNullOrEmpty(texPath.ToString())) continue;
|
|
|
|
var texResource = factory.Create(texPath.ToString());
|
|
var cachedTexResource = cachedResources.First(r => r.IsReplacedByThis(texResource));
|
|
cache.AddAssociatedResource(cachedTexResource, cachedMdlResource, cachedMtrlResource);
|
|
}
|
|
}
|
|
}
|
|
|
|
return cache;
|
|
}
|
|
|
|
public List<FileReplacement> PrintRequestedResources()
|
|
{
|
|
var cache = BuildCharacterCache();
|
|
|
|
PluginLog.Debug("--- CURRENTLY LOADED FILES ---");
|
|
|
|
PluginLog.Debug(cache.ToString());
|
|
|
|
PluginLog.Debug("--- LOOSE FILES ---");
|
|
|
|
foreach (var resource in cachedResources.Where(r => !r.IsInUse).OrderBy(a => a.GamePath))
|
|
{
|
|
PluginLog.Debug(resource.ToString());
|
|
}
|
|
|
|
return cache.FileReplacements;
|
|
}
|
|
|
|
public void StartHooks()
|
|
{
|
|
cachedResources.Clear();
|
|
SetupHumanHooks();
|
|
EnableHumanHooks();
|
|
PluginLog.Debug("Hooks enabled");
|
|
}
|
|
|
|
public void StopHooks()
|
|
{
|
|
DisableHumanHooks();
|
|
DisposeHumanHooks();
|
|
}
|
|
|
|
private void AddRequestedResource(FileReplacement replacement)
|
|
{
|
|
if (!cachedResources.Any(a => a.IsReplacedByThis(replacement)))
|
|
{
|
|
cachedResources.Add(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.Debug("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);
|
|
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 = new(loadedMaterials.Except(new[] { existingMat }));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
PluginLog.Error(ex, "error");
|
|
}
|
|
}
|
|
|
|
private byte LoadMtrlTexDetour(IntPtr mtrlResourceHandle)
|
|
{
|
|
if (clientState.LocalPlayer != null)
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|