using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Framework; namespace NominaOcculta; internal class GameFunctions : IDisposable { private static class Signatures { internal const string GenerateName = "E8 ?? ?? ?? ?? 48 8D 8B ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 85 C0 74 1B 48 8D 8B ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 8B D0 E8 ?? ?? ?? ?? 48 8B CB 48 8B 7C 24"; internal const string Utf8StringCtor = "E8 ?? ?? ?? ?? 44 2B F7"; internal const string Utf8StringDtor = "80 79 21 00 75 12"; internal const string AtkTextNodeSetText = "E8 ?? ?? ?? ?? 8D 4E 32"; internal const string LoadExd = "40 53 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 41 0F B6 D9"; internal const string CharacterInitialise = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 48 8B F9 48 8B EA 48 81 C1 ?? ?? ?? ?? E8"; internal const string CharacterIsMount = "40 53 48 83 EC 20 48 8B 01 48 8B D9 FF 50 10 83 F8 08 75 08"; internal const string FlagSlotUpdate = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B DA 49 8B F0 48 8B F9 83 FA 0A"; } #region Delegates private delegate IntPtr GenerateNameDelegate(int race, int clan, int gender, IntPtr first, IntPtr last); private delegate IntPtr Utf8StringCtorDelegate(IntPtr memory); private delegate void Utf8StringDtorDelegate(IntPtr memory); private delegate void AtkTextNodeSetTextDelegate(IntPtr node, IntPtr text); private delegate byte LoadExdDelegate(IntPtr a1, string sheetName, byte a3, byte a4); private delegate IntPtr GetExcelModuleDelegate(IntPtr uiModule); private delegate IntPtr CharacterIsMountDelegate(IntPtr actor); private delegate char CharacterInitialiseDelegate(IntPtr actorPtr, IntPtr customizeDataPtr); private delegate IntPtr FlagSlotUpdateDelegate(IntPtr actorPtr, uint slot, IntPtr equipData); #endregion #region Functions [Signature(Signatures.Utf8StringCtor)] private Utf8StringCtorDelegate Utf8StringCtor { get; init; } = null!; [Signature(Signatures.Utf8StringDtor)] private Utf8StringDtorDelegate Utf8StringDtor { get; init; } = null!; [Signature(Signatures.GenerateName)] private GenerateNameDelegate InternalGenerateName { get; init; } = null!; [Signature(Signatures.LoadExd)] private LoadExdDelegate LoadExd { get; init; } = null!; #endregion #region Hooks [Signature(Signatures.AtkTextNodeSetText, DetourName = nameof(AtkTextNodeSetTextDetour))] private Hook AtkTextNodeSetTextHook { get; init; } = null!; [Signature(Signatures.CharacterIsMount, DetourName = nameof(CharacterIsMountDetour))] private Hook CharacterIsMountHook { get; init; } = null!; [Signature(Signatures.CharacterInitialise, DetourName = nameof(CharacterInitialiseDetour))] private Hook CharacterInitializeHook { get; init; } = null!; [Signature(Signatures.FlagSlotUpdate, DetourName = nameof(FlagSlotUpdateDetour))] private Hook FlagSlotUpdateHook { get; init; } = null!; #endregion #region Events internal delegate void AtkTextNodeSetTextEventDelegate(IntPtr node, IntPtr text, ref SeString? overwrite); internal event AtkTextNodeSetTextEventDelegate? AtkTextNodeSetText; internal unsafe delegate void CharacterInitialiseEventDelegate(GameObject* gameObj, IntPtr humanPtr, IntPtr customiseDataPtr); internal event CharacterInitialiseEventDelegate? CharacterInitialise; internal unsafe delegate void FlagSlotUpdateEventDelegate(GameObject* gameObj, uint slot, EquipData* equipData); internal event FlagSlotUpdateEventDelegate? FlagSlotUpdate; #endregion private Plugin Plugin { get; } private IntPtr First { get; } private IntPtr Last { get; } private Dictionary HumansToIds { get; } = new(); internal GameFunctions(Plugin plugin) { this.Plugin = plugin; SignatureHelper.Initialise(this); this.AtkTextNodeSetTextHook.Enable(); this.CharacterInitializeHook.Enable(); this.CharacterIsMountHook.Enable(); this.FlagSlotUpdateHook.Enable(); this.First = Marshal.AllocHGlobal(128); this.Last = Marshal.AllocHGlobal(128); this.Utf8StringCtor(this.First); this.Utf8StringCtor(this.Last); this.Plugin.ClientState.TerritoryChanged += this.OnTerritoryChange; } public void Dispose() { this.Plugin.ClientState.TerritoryChanged -= this.OnTerritoryChange; this.Utf8StringDtor(this.Last); this.Utf8StringDtor(this.First); Marshal.FreeHGlobal(this.Last); Marshal.FreeHGlobal(this.First); this.AtkTextNodeSetTextHook.Dispose(); this.CharacterInitializeHook.Dispose(); this.CharacterIsMountHook.Dispose(); this.FlagSlotUpdateHook.Dispose(); } private void OnTerritoryChange(object? sender, ushort e) { this.HumansToIds.Clear(); } private unsafe void AtkTextNodeSetTextDetour(IntPtr node, IntPtr text) { SeString? overwrite = null; this.AtkTextNodeSetText?.Invoke(node, text, ref overwrite); if (overwrite != null) { fixed (byte* newText = overwrite.Encode().Terminate()) { this.AtkTextNodeSetTextHook.Original(node, (IntPtr) newText); } return; } this.AtkTextNodeSetTextHook.Original(node, text); } private IntPtr _lastActor = IntPtr.Zero; private unsafe IntPtr CharacterIsMountDetour(IntPtr characterPtr) { var chara = (GameObject*) characterPtr; if (chara != null && chara->ObjectKind == (byte) ObjectKind.Pc) { this._lastActor = characterPtr; } else { this._lastActor = IntPtr.Zero; } return this.CharacterIsMountHook.Original(characterPtr); } private unsafe char CharacterInitialiseDetour(IntPtr actorPtr, IntPtr customizeDataPtr) { if (this._lastActor != IntPtr.Zero) { try { var id = ((GameObject*) this._lastActor)->ObjectID; // remove other humans that may have had this object id foreach (var (human, objId) in this.HumansToIds.ToList()) { if (objId == id) { this.HumansToIds.Remove(human); } } this.HumansToIds[actorPtr] = id; this.CharacterInitialise?.Invoke((GameObject*) this._lastActor, actorPtr, customizeDataPtr); this._lastActor = IntPtr.Zero; } catch (Exception e) { PluginLog.LogError(e, "yeet"); } } return this.CharacterInitializeHook.Original(actorPtr, customizeDataPtr); } private unsafe IntPtr FlagSlotUpdateDetour(IntPtr actorPtr, uint slot, IntPtr equipDataPtr) { if (this.HumansToIds.TryGetValue(actorPtr, out var objId)) { var obj = this.Plugin.ObjectTable.FirstOrDefault(obj => obj.ObjectId == objId); if (obj != null) { try { this.FlagSlotUpdate?.Invoke((GameObject*) obj.Address, slot, (EquipData*) equipDataPtr); } catch (Exception e) { PluginLog.LogError(e, "yeet3"); } } } return this.FlagSlotUpdateHook.Original(actorPtr, slot, equipDataPtr); } public string? GenerateName(int race, int clan, int gender) { if (this.InternalGenerateName(race, clan, gender, this.First, this.Last) == IntPtr.Zero) { return null; } var first = Marshal.PtrToStringUTF8(Marshal.ReadIntPtr(this.First)); var last = Marshal.PtrToStringUTF8(Marshal.ReadIntPtr(this.Last)); if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(last)) { return null; } return $"{first} {last}"; } public unsafe void LoadSheet(string name) { var ui = (IntPtr) Framework.Instance()->GetUiModule(); var getExcelModulePtr = *(*(IntPtr**) ui + 5); var getExcelModule = Marshal.GetDelegateForFunctionPointer(getExcelModulePtr); var excelModule = getExcelModule(ui); var exdModule = *(IntPtr*) (excelModule + 8); var excel = *(IntPtr*) (exdModule + 0x20); this.LoadExd(excel, name, 0, 1); } }