Refactor/clean up Chat

This commit is contained in:
Asriel Camora
2023-11-16 22:30:55 -08:00
parent 5979b8f6f5
commit e72c5294de
5 changed files with 27 additions and 157 deletions
+2
View File
@@ -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();
+1
View File
@@ -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
+22 -155
View File
@@ -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<Utf8String*, int, IntPtr, void> SanitiseString = null!;
static Chat()
public void SendMessage(string message)
{
if (Service.SigScanner.TryScanText(Signatures.SendChat, out var processChatBoxPtr))
{
ProcessChatBox = Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(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<Utf8String*, int, IntPtr, void>)sanitisePtr;
}
}
}
/// <summary>
/// <para>
/// Send a given message to the chat box. <b>This can send chat to the server.</b>
/// </para>
/// <para>
/// <b>This method is unsafe.</b> 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.
/// </para>
/// </summary>
/// <param name="message">Message to send</param>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
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);
}
/// <summary>
/// <para>
/// Send a given message to the chat box. <b>This can send chat to the server.</b>
/// </para>
/// <para>
/// This method is slightly less unsafe than <see cref="SendMessageUnsafe"/>. 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.
/// </para>
/// </summary>
/// <param name="message">message to send</param>
/// <exception cref="ArgumentException">If <paramref name="message"/> is empty, longer than 500 bytes in UTF-8, or contains invalid characters.</exception>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
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);
}
/// <summary>
/// <para>
/// Sanitises a string by removing any invalid input.
/// </para>
/// <para>
/// The result of this method is safe to use with
/// <see cref="SendMessage"/>, provided that it is not empty or too
/// long.
/// </para>
/// </summary>
/// <param name="text">text to sanitise</param>
/// <returns>sanitised text</returns>
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
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);
}
}
+1 -1
View File
@@ -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}");
}
+1 -1
View File
@@ -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;
}
}