using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Runtime.InteropServices; using System.Text; using Dalamud.Game; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Client.System.String; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Questionable.Model.Questing; namespace Questionable.Functions; internal sealed class ChatFunctions { private delegate void ProcessChatBoxDelegate(nint uiModule, nint message, nint unused, byte a4); private static class Signatures { internal const string SendChat = "48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F2 48 8B F9 45 84 C9"; } [StructLayout(LayoutKind.Explicit)] private readonly struct ChatPayload : IDisposable { [FieldOffset(0)] private readonly nint textPtr; [FieldOffset(16)] private readonly ulong textLen; [FieldOffset(8)] private readonly ulong unk1; [FieldOffset(24)] private readonly ulong unk2; internal ChatPayload(byte[] stringBytes) { textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30); Marshal.Copy(stringBytes, 0, textPtr, stringBytes.Length); Marshal.WriteByte(textPtr + stringBytes.Length, 0); textLen = (ulong)(stringBytes.Length + 1); unk1 = 64uL; unk2 = 0uL; } public void Dispose() { Marshal.FreeHGlobal(textPtr); } } private readonly ReadOnlyDictionary _emoteCommands; private readonly GameFunctions _gameFunctions; private readonly ITargetManager _targetManager; private readonly ILogger _logger; private readonly ProcessChatBoxDelegate _processChatBox; public ChatFunctions(ISigScanner sigScanner, IDataManager dataManager, GameFunctions gameFunctions, ITargetManager targetManager, ILogger logger) { _gameFunctions = gameFunctions; _targetManager = targetManager; _logger = logger; _processChatBox = Marshal.GetDelegateForFunctionPointer(sigScanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F2 48 8B F9 45 84 C9")); _emoteCommands = (from x in dataManager.GetExcelSheet() where x.RowId != 0 where x.TextCommand.IsValid select (RowId: x.RowId, Command: x.TextCommand.Value.Command.ToString()) into x where !string.IsNullOrEmpty(x.Command) && x.Command.StartsWith('/') select x).ToDictionary<(uint, string), EEmote, string>(((uint RowId, string Command) x) => (EEmote)x.RowId, ((uint RowId, string Command) x) => x.Command).AsReadOnly(); } private unsafe void SendMessageUnsafe(byte[] message) { nint uIModule = (nint)Framework.Instance()->GetUIModule(); using ChatPayload structure = new ChatPayload(message); nint num = Marshal.AllocHGlobal(400); Marshal.StructureToPtr(structure, num, fDeleteOld: false); _processChatBox(uIModule, num, IntPtr.Zero, 0); Marshal.FreeHGlobal(num); } private void SendMessage(string message) { _logger.LogDebug("Attempting to send chat message '{Message}'", message); byte[] bytes = Encoding.UTF8.GetBytes(message); if (bytes.Length == 0) { throw new ArgumentException("message is empty", "message"); } if (bytes.Length > 500) { throw new ArgumentException("message is longer than 500 bytes", "message"); } if (message.Length != SanitiseText(message).Length) { throw new ArgumentException("message contained invalid characters", "message"); } SendMessageUnsafe(bytes); } private unsafe string SanitiseText(string text) { Utf8String* intPtr = Utf8String.FromString(text); intPtr->SanitizeString(AllowedEntities.UppercaseLetters | AllowedEntities.LowercaseLetters | AllowedEntities.Numbers | AllowedEntities.SpecialCharacters | AllowedEntities.CharacterList | AllowedEntities.OtherCharacters | AllowedEntities.Payloads | AllowedEntities.Unknown9, null); string result = intPtr->ToString(); intPtr->Dtor(); IMemorySpace.Free(intPtr); return result; } public void ExecuteCommand(string command) { if (command.StartsWith('/')) { SendMessage(command); } } public void UseEmote(uint dataId, EEmote emote) { IGameObject gameObject = _gameFunctions.FindObjectByDataId(dataId); if (gameObject != null) { _targetManager.Target = gameObject; ExecuteCommand(_emoteCommands[emote] + " motion"); } } public void UseEmote(EEmote emote) { ExecuteCommand(_emoteCommands[emote] + " motion"); } }