Files
ClubPenguinClient/MareSynchronos/Hooks/DrawHooks.cs
2022-06-14 15:12:02 +02:00

418 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 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();
private CharacterCache characterCache;
public DrawHooks(DalamudPluginInterface pluginInterface, ClientState clientState, ObjectTable objectTable, FileReplacementFactory factory, GameGui gameGui)
{
this.pluginInterface = pluginInterface;
this.clientState = clientState;
this.objectTable = objectTable;
this.factory = factory;
this.gameGui = gameGui;
characterCache = new CharacterCache();
SignatureHelper.Initialise(this);
}
public delegate IntPtr CharacterBaseCreateDelegate(uint a, IntPtr b, IntPtr c, byte d);
public delegate void CharacterBaseDestructorDelegate(IntPtr drawBase);
public delegate void EnableDrawDelegate(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d);
public delegate IntPtr GeneralResolveDelegate(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4);
public delegate byte LoadMtrlFilesDelegate(IntPtr mtrlResourceHandle);
public delegate IntPtr MaterialResolveDetour(IntPtr drawObject, IntPtr path, IntPtr unk3, uint unk4, ulong unk5);
public delegate void OnModelLoadCompleteDelegate(IntPtr drawObject);
public void Dispose()
{
DisableHumanHooks();
DisposeHumanHooks();
}
public CharacterCache BuildCharacterCache()
{
foreach (var resource in cachedResources)
{
resource.IsInUse = false;
resource.ImcData = string.Empty;
}
var cache = new CharacterCache();
PluginLog.Debug("Invaldated character cache");
var model = (CharacterBase*)((Character*)clientState.LocalPlayer!.Address)->GameObject.GetDrawObject();
for (var idx = 0; idx < model->SlotCount; ++idx)
{
var mdl = (RenderModel*)model->ModelArray[idx];
if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara)
{
continue;
}
var mdlResource = factory.Create(new Utf8String(mdl->ResourceHandle->FileName()).ToString());
var cachedMdlResource = cachedResources.First(r => r.IsReplacedByThis(mdlResource));
var imc = (ResourceHandle*)model->IMCArray[idx];
if (imc != null)
{
byte[] imcData = new byte[imc->Data->DataLength];
Marshal.Copy((IntPtr)imc->Data->DataPtr, imcData, 0, (int)imc->Data->DataLength);
string imcDataStr = BitConverter.ToString(imcData).Replace("-", "");
cachedMdlResource.ImcData = imcDataStr;
}
cache.AddAssociatedResource(cachedMdlResource, null!, null!);
for (int mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++)
{
var mtrl = (Material*)mdl->Materials[mtrlIdx];
if (mtrl == null) continue;
var mtrlFileResource = factory.Create(new Utf8String(mtrl->ResourceHandle->FileName()).ToString().Split("|")[2]);
var cachedMtrlResource = cachedResources.First(r => r.IsReplacedByThis(mtrlFileResource));
cache.AddAssociatedResource(cachedMtrlResource, cachedMdlResource, null!);
var mtrlResource = (MtrlResource*)mtrl->ResourceHandle;
for (int resIdx = 0; resIdx < mtrlResource->NumTex; resIdx++)
{
var texPath = new Utf8String(mtrlResource->TexString(resIdx));
if (string.IsNullOrEmpty(texPath.ToString())) continue;
var texResource = factory.Create(texPath.ToString());
var cachedTexResource = cachedResources.First(r => r.IsReplacedByThis(texResource));
cache.AddAssociatedResource(cachedTexResource, cachedMdlResource, cachedMtrlResource);
}
}
}
return cache;
}
public List<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)
{
PluginLog.Debug("Character base detour");
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 (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)
{
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)
{
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;
}
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);
}
}
}