Refactor/clean up Chat
This commit is contained in:
@@ -33,6 +33,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
public Configuration Configuration { get; }
|
public Configuration Configuration { get; }
|
||||||
public Hooks Hooks { get; }
|
public Hooks Hooks { get; }
|
||||||
|
public Chat Chat { get; }
|
||||||
public IconManager IconManager { get; }
|
public IconManager IconManager { get; }
|
||||||
|
|
||||||
public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface)
|
public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface)
|
||||||
@@ -42,6 +43,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
WindowSystem = new("Craftimizer");
|
WindowSystem = new("Craftimizer");
|
||||||
Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new();
|
Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new();
|
||||||
Hooks = new();
|
Hooks = new();
|
||||||
|
Chat = new();
|
||||||
IconManager = new();
|
IconManager = new();
|
||||||
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public sealed class Service
|
|||||||
public static Plugin Plugin { get; private set; }
|
public static Plugin Plugin { get; private set; }
|
||||||
public static Configuration Configuration => Plugin.Configuration;
|
public static Configuration Configuration => Plugin.Configuration;
|
||||||
public static WindowSystem WindowSystem => Plugin.WindowSystem;
|
public static WindowSystem WindowSystem => Plugin.WindowSystem;
|
||||||
|
public static Chat Chat => Plugin.Chat;
|
||||||
public static IconManager IconManager => Plugin.IconManager;
|
public static IconManager IconManager => Plugin.IconManager;
|
||||||
#pragma warning restore CS8618
|
#pragma warning restore CS8618
|
||||||
|
|
||||||
|
|||||||
+22
-155
@@ -1,170 +1,37 @@
|
|||||||
|
using Craftimizer.Plugin;
|
||||||
|
using Dalamud.Utility.Signatures;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using System;
|
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
|
// 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";
|
Service.GameInteropProvider.InitializeFromAttributes(this);
|
||||||
internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private delegate void ProcessChatBoxDelegate(UIModule* uiModule, IntPtr message, IntPtr unused, byte a4);
|
public void SendMessage(string message)
|
||||||
|
|
||||||
private static ProcessChatBoxDelegate? ProcessChatBox { get; }
|
|
||||||
|
|
||||||
private static readonly unsafe delegate* unmanaged<Utf8String*, int, IntPtr, void> SanitiseString = null!;
|
|
||||||
|
|
||||||
static Chat()
|
|
||||||
{
|
{
|
||||||
if (Service.SigScanner.TryScanText(Signatures.SendChat, out var processChatBoxPtr))
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
{
|
throw new ArgumentException("Message is empty", nameof(message));
|
||||||
ProcessChatBox = Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(processChatBoxPtr);
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe
|
var str = Utf8String.FromString(message);
|
||||||
{
|
sanitizeString(str, 0x27F, null);
|
||||||
if (Service.SigScanner.TryScanText(Signatures.SanitiseString, out var sanitisePtr))
|
sendChat(Framework.Instance()->GetUiModule(), str, null, false);
|
||||||
{
|
str->Dtor(true);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable
|
|||||||
if (gearsetId.HasValue)
|
if (gearsetId.HasValue)
|
||||||
{
|
{
|
||||||
if (ImGuiUtils.ButtonCentered("Switch Job"))
|
if (ImGuiUtils.ButtonCentered("Switch Job"))
|
||||||
Chat.SendMessage($"/gearset change {gearsetId + 1}");
|
Service.Chat.SendMessage($"/gearset change {gearsetId + 1}");
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGui.SetTooltip($"Swap to gearset {gearsetId + 1}");
|
ImGui.SetTooltip($"Swap to gearset {gearsetId + 1}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ public sealed unsafe class SynthHelper : Window, IDisposable
|
|||||||
{
|
{
|
||||||
if (canExecute && i == 0)
|
if (canExecute && i == 0)
|
||||||
{
|
{
|
||||||
Chat.SendMessage($"/ac \"{action.GetName(RecipeData.ClassJob)}\"");
|
Service.Chat.SendMessage($"/ac \"{action.GetName(RecipeData.ClassJob)}\"");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user