qstbak/Questionable/Questionable.Functions/ChatFunctions.cs
2025-10-09 07:47:19 +10:00

145 lines
4.5 KiB
C#

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<EEmote, string> _emoteCommands;
private readonly GameFunctions _gameFunctions;
private readonly ITargetManager _targetManager;
private readonly ILogger<ChatFunctions> _logger;
private readonly ProcessChatBoxDelegate _processChatBox;
public ChatFunctions(ISigScanner sigScanner, IDataManager dataManager, GameFunctions gameFunctions, ITargetManager targetManager, ILogger<ChatFunctions> logger)
{
_gameFunctions = gameFunctions;
_targetManager = targetManager;
_logger = logger;
_processChatBox = Marshal.GetDelegateForFunctionPointer<ProcessChatBoxDelegate>(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<Emote>()
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");
}
}