using Dalamud.Hooking; using Lumina.Excel.GeneratedSheets; using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using XIVChatCommon; namespace XIVChatPlugin { public class GameFunctions : IDisposable { private readonly Plugin plugin; private delegate IntPtr GetUIBaseDelegate(); private delegate IntPtr GetUIModuleDelegate(IntPtr basePtr); private delegate void EasierProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4); private delegate byte RequestFriendListDelegate(IntPtr manager); private delegate int FormatFriendListNameDelegate(long a1, long a2, long a3, int a4, IntPtr data, long a6); private delegate IntPtr OnReceiveFriendListChunkDelegate(IntPtr a1, IntPtr data); private readonly Hook friendListHook; private readonly Hook formatHook; private readonly Hook receiveChunkHook; private readonly GetUIModuleDelegate GetUIModule; private readonly EasierProcessChatBoxDelegate _EasierProcessChatBox; private readonly IntPtr uiModulePtr; private IntPtr friendListManager = IntPtr.Zero; private bool requestingFriendList = false; public bool RequestingFriendList => this.requestingFriendList; private readonly List friends = new List(); public delegate void ReceiveFriendListHandler(List friends); public event ReceiveFriendListHandler ReceiveFriendList; public GameFunctions(Plugin plugin) { this.plugin = plugin ?? throw new ArgumentNullException(nameof(plugin), "Plugin cannot be null"); var getUIModulePtr = this.plugin.Interface.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 48 83 7F ?? 00 48 8B F0"); var easierProcessChatBoxPtr = this.plugin.Interface.TargetModuleScanner.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9"); var friendListPtr = this.plugin.Interface.TargetModuleScanner.ScanText("40 53 48 81 EC 80 0F 00 00 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B D9 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 44 0F B6 43 ?? 33 C9"); var formatPtr = this.plugin.Interface.TargetModuleScanner.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 41 56 48 83 EC 30 48 8B 6C 24 ??"); var recvChunkPtr = this.plugin.Interface.TargetModuleScanner.ScanText("48 89 5C 24 ?? 56 48 83 EC 20 48 8B 0D ?? ?? ?? ?? 48 8B F2"); this.uiModulePtr = this.plugin.Interface.TargetModuleScanner.GetStaticAddressFromSig("48 8B 0D ?? ?? ?? ?? 48 8D 54 24 ?? 48 83 C1 10 E8 ?? ?? ?? ??"); this.GetUIModule = Marshal.GetDelegateForFunctionPointer(getUIModulePtr); this._EasierProcessChatBox = Marshal.GetDelegateForFunctionPointer(easierProcessChatBoxPtr); this.friendListHook = new Hook(friendListPtr, new RequestFriendListDelegate(this.OnRequestFriendList)); this.formatHook = new Hook(formatPtr, new FormatFriendListNameDelegate(this.OnFormatFriendList)); this.receiveChunkHook = new Hook(recvChunkPtr, new OnReceiveFriendListChunkDelegate(this.OnReceiveFriendList)); this.friendListHook.Enable(); this.formatHook.Enable(); this.receiveChunkHook.Enable(); } public void ProcessChatBox(string message) { IntPtr uiModule = this.GetUIModule(Marshal.ReadIntPtr(this.uiModulePtr)); if (uiModule == IntPtr.Zero) { throw new ApplicationException("uiModule was null"); } using (var payload = new ChatPayload(message)) { IntPtr mem1 = Marshal.AllocHGlobal(400); Marshal.StructureToPtr(payload, mem1, false); this._EasierProcessChatBox(uiModule, mem1, IntPtr.Zero, 0); Marshal.FreeHGlobal(mem1); } } public bool RequestFriendList() { if (this.friendListManager == IntPtr.Zero || this.friendListHook == null) { return false; } this.requestingFriendList = true; this.friendListHook.Original(this.friendListManager); return true; } private byte OnRequestFriendList(IntPtr manager) { this.friendListManager = manager; return this.friendListHook.Original(manager); } private int OnFormatFriendList(long a1, long a2, long a3, int a4, IntPtr data, long a6) { // have to call this first to populate cross-world info var ret = this.formatHook.Original(a1, a2, a3, a4, data, a6); if (!this.RequestingFriendList) { return ret; } var entry = Marshal.PtrToStructure(data); string jobName = null; if (entry.job > 0) { jobName = this.plugin.Interface.Data.GetExcelSheet().GetRow(entry.job)?.Name; } var player = new Player { Name = entry.Name(), FreeCompany = entry.FreeCompany(), Status = entry.flags, CurrentWorld = entry.currentWorldId, CurrentWorldName = this.plugin.Interface.Data.GetExcelSheet().GetRow(entry.currentWorldId)?.Name, HomeWorld = entry.homeWorldId, HomeWorldName = this.plugin.Interface.Data.GetExcelSheet().GetRow(entry.homeWorldId)?.Name, Territory = entry.territoryId, TerritoryName = this.plugin.Interface.Data.GetExcelSheet().GetRow(entry.territoryId)?.PlaceName?.Value?.Name, Job = entry.job, JobName = jobName, GrandCompany = entry.grandCompany, GrandCompanyName = this.plugin.Interface.Data.GetExcelSheet().GetRow(entry.grandCompany)?.Name, Languages = entry.langsEnabled, MainLanguage = entry.mainLanguage, }; this.friends.Add(player); return ret; } private IntPtr OnReceiveFriendList(IntPtr a1, IntPtr data) { var ret = this.receiveChunkHook.Original(a1, data); // + 0xc // 1 = party // 2 = friends // 3 = linkshell // doesn't run (though same memory gets updated) for cwl or blacklist // + 0x8 is current number of results returned or 0 when end of list if (!this.RequestingFriendList) { goto Return; } if (Marshal.ReadByte(data + 0xc) != 2 || Marshal.ReadInt16(data + 0x8) != 0) { goto Return; } this.ReceiveFriendList(this.friends); this.friends.Clear(); this.requestingFriendList = false; Return: return ret; } public void Dispose() { this.friendListHook?.Dispose(); this.formatHook?.Dispose(); this.receiveChunkHook?.Dispose(); } } [StructLayout(LayoutKind.Explicit)] struct ChatPayload : IDisposable { [FieldOffset(0)] readonly IntPtr textPtr; [FieldOffset(16)] readonly ulong textLen; [FieldOffset(8)] readonly ulong unk1; [FieldOffset(24)] readonly ulong unk2; internal ChatPayload(string text) { byte[] stringBytes = Encoding.UTF8.GetBytes(text); this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30); Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length); Marshal.WriteByte(this.textPtr + stringBytes.Length, 0); this.textLen = (ulong)(stringBytes.Length + 1); this.unk1 = 64; this.unk2 = 0; } public void Dispose() { Marshal.FreeHGlobal(this.textPtr); } } [StructLayout(LayoutKind.Sequential)] struct FriendListEntryRaw { readonly ulong unk1; internal ulong flags; readonly uint unk2; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] readonly byte[] unk3; internal readonly ushort currentWorldId; internal readonly ushort homeWorldId; internal readonly ushort territoryId; internal readonly byte grandCompany; internal readonly byte mainLanguage; internal readonly byte langsEnabled; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] readonly byte[] unk4; internal readonly byte job; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] readonly byte[] name; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] readonly byte[] fc; private static string HandleString(byte[] bytes) { byte[] nonNull = bytes.TakeWhile(b => b != 0).ToArray(); if (nonNull.Length == 0) { return null; } return Encoding.UTF8.GetString(nonNull); } public string Name() => HandleString(this.name); public string FreeCompany() => HandleString(this.fc); } }