diff --git a/Craftimizer/Plugin.cs b/Craftimizer/Plugin.cs index e3db6c0..0c06ef4 100644 --- a/Craftimizer/Plugin.cs +++ b/Craftimizer/Plugin.cs @@ -33,6 +33,7 @@ public sealed class Plugin : IDalamudPlugin public Configuration Configuration { get; } public Hooks Hooks { get; } + public Chat Chat { get; } public IconManager IconManager { get; } public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface) @@ -42,6 +43,7 @@ public sealed class Plugin : IDalamudPlugin WindowSystem = new("Craftimizer"); Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new(); Hooks = new(); + Chat = new(); IconManager = new(); var assembly = Assembly.GetExecutingAssembly(); diff --git a/Craftimizer/Service.cs b/Craftimizer/Service.cs index 91a4a98..5ea5fb6 100644 --- a/Craftimizer/Service.cs +++ b/Craftimizer/Service.cs @@ -28,6 +28,7 @@ public sealed class Service public static Plugin Plugin { get; private set; } public static Configuration Configuration => Plugin.Configuration; public static WindowSystem WindowSystem => Plugin.WindowSystem; + public static Chat Chat => Plugin.Chat; public static IconManager IconManager => Plugin.IconManager; #pragma warning restore CS8618 diff --git a/Craftimizer/Utils/Chat.cs b/Craftimizer/Utils/Chat.cs index 3efdd0a..45732a9 100644 --- a/Craftimizer/Utils/Chat.cs +++ b/Craftimizer/Utils/Chat.cs @@ -1,170 +1,37 @@ +using Craftimizer.Plugin; +using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Client.System.Framework; -using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI; using System; -using System.Runtime.InteropServices; -using System.Text; -namespace Craftimizer.Plugin.Utils; +namespace Craftimizer.Utils; // https://github.com/Caraxi/SimpleTweaksPlugin/blob/0973b93931cdf8a1b01153984d62f76d998747ff/Utility/ChatHelper.cs#L17 -public static unsafe class Chat +public sealed unsafe class Chat { - private static class Signatures + private delegate void SendChatDelegate(UIModule* uiModule, Utf8String* message, Utf8String* historyMessage, bool pushToHistory); + private delegate void SanitizeStringDelegate(Utf8String* data, int flags, Utf8String* buffer); + + [Signature("48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9")] + private readonly SendChatDelegate sendChat = null!; + + [Signature("E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D")] + private readonly SanitizeStringDelegate sanitizeString = null!; + + public Chat() { - internal const string SendChat = "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9"; - internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D"; + Service.GameInteropProvider.InitializeFromAttributes(this); } - private delegate void ProcessChatBoxDelegate(UIModule* uiModule, IntPtr message, IntPtr unused, byte a4); - - private static ProcessChatBoxDelegate? ProcessChatBox { get; } - - private static readonly unsafe delegate* unmanaged SanitiseString = null!; - - static Chat() + public void SendMessage(string message) { - if (Service.SigScanner.TryScanText(Signatures.SendChat, out var processChatBoxPtr)) - { - ProcessChatBox = Marshal.GetDelegateForFunctionPointer(processChatBoxPtr); - } + if (string.IsNullOrWhiteSpace(message)) + throw new ArgumentException("Message is empty", nameof(message)); - unsafe - { - if (Service.SigScanner.TryScanText(Signatures.SanitiseString, out var sanitisePtr)) - { - SanitiseString = (delegate* unmanaged)sanitisePtr; - } - } - } - - /// - /// - /// Send a given message to the chat box. This can send chat to the server. - /// - /// - /// This method is unsafe. This method does no checking on your input and - /// may send content to the server that the normal client could not. You must - /// verify what you're sending and handle content and length to properly use - /// this. - /// - /// - /// Message to send - /// If the signature for this function could not be found - public static unsafe void SendMessageUnsafe(byte[] message) - { - if (ProcessChatBox == null) - { - throw new InvalidOperationException("Could not find signature for chat sending"); - } - - var uiModule = Framework.Instance()->GetUiModule(); - - using var payload = new ChatPayload(message); - var mem1 = Marshal.AllocHGlobal(400); - Marshal.StructureToPtr(payload, mem1, false); - - ProcessChatBox(uiModule, mem1, IntPtr.Zero, 0); - - Marshal.FreeHGlobal(mem1); - } - - /// - /// - /// Send a given message to the chat box. This can send chat to the server. - /// - /// - /// This method is slightly less unsafe than . It - /// will throw exceptions for certain inputs that the client can't normally send, - /// but it is still possible to make mistakes. Use with caution. - /// - /// - /// message to send - /// If is empty, longer than 500 bytes in UTF-8, or contains invalid characters. - /// If the signature for this function could not be found - public static void SendMessage(string message) - { - var bytes = Encoding.UTF8.GetBytes(message); - if (bytes.Length == 0) - { - throw new ArgumentException("message is empty", nameof(message)); - } - - if (bytes.Length > 500) - { - throw new ArgumentException("message is longer than 500 bytes", nameof(message)); - } - - if (message.Length != SanitiseText(message).Length) - { - throw new ArgumentException("message contained invalid characters", nameof(message)); - } - - SendMessageUnsafe(bytes); - } - - /// - /// - /// Sanitises a string by removing any invalid input. - /// - /// - /// The result of this method is safe to use with - /// , provided that it is not empty or too - /// long. - /// - /// - /// text to sanitise - /// sanitised text - /// If the signature for this function could not be found - public static unsafe string SanitiseText(string text) - { - if (SanitiseString == null) - { - throw new InvalidOperationException("Could not find signature for chat sanitisation"); - } - - var uText = Utf8String.FromString(text); - - SanitiseString(uText, 0x27F, IntPtr.Zero); - var sanitised = uText->ToString(); - - uText->Dtor(); - IMemorySpace.Free(uText); - - return sanitised; - } - - [StructLayout(LayoutKind.Explicit)] - private readonly struct ChatPayload : IDisposable - { - [FieldOffset(0)] - private readonly IntPtr 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 = 64; - unk2 = 0; - } - - public void Dispose() - { - Marshal.FreeHGlobal(textPtr); - } + var str = Utf8String.FromString(message); + sanitizeString(str, 0x27F, null); + sendChat(Framework.Instance()->GetUiModule(), str, null, false); + str->Dtor(true); } } diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index c80a3d8..8c8c8ab 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -360,7 +360,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable if (gearsetId.HasValue) { if (ImGuiUtils.ButtonCentered("Switch Job")) - Chat.SendMessage($"/gearset change {gearsetId + 1}"); + Service.Chat.SendMessage($"/gearset change {gearsetId + 1}"); if (ImGui.IsItemHovered()) ImGui.SetTooltip($"Swap to gearset {gearsetId + 1}"); } diff --git a/Craftimizer/Windows/SynthHelper.cs b/Craftimizer/Windows/SynthHelper.cs index 178bd69..0cf46be 100644 --- a/Craftimizer/Windows/SynthHelper.cs +++ b/Craftimizer/Windows/SynthHelper.cs @@ -230,7 +230,7 @@ public sealed unsafe class SynthHelper : Window, IDisposable { if (canExecute && i == 0) { - Chat.SendMessage($"/ac \"{action.GetName(RecipeData.ClassJob)}\""); + Service.Chat.SendMessage($"/ac \"{action.GetName(RecipeData.ClassJob)}\""); break; } }