145 lines
4.5 KiB
C#
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");
|
|
}
|
|
}
|