XIVChat/XIVChatPlugin/GameFunctions.cs

370 lines
15 KiB
C#
Raw Normal View History

2020-10-23 21:24:32 +00:00
using Dalamud.Hooking;
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
2020-10-23 21:24:32 +00:00
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
2021-01-07 21:26:27 +00:00
using Dalamud.Plugin;
using XIVChatCommon.Message;
2020-10-23 21:24:32 +00:00
namespace XIVChatPlugin {
public class GameFunctions : IDisposable {
private const int ChannelOffset = 4048; // update 5.4-HF1
2021-01-07 21:26:27 +00:00
private Plugin Plugin { get; }
2020-10-23 21:24:32 +00:00
2021-01-07 21:26:27 +00:00
private delegate IntPtr GetUiModuleDelegate(IntPtr basePtr);
2020-10-23 21:24:32 +00:00
private delegate void EasierProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4);
2020-10-23 21:24:32 +00:00
private delegate byte RequestFriendListDelegate(IntPtr manager);
2020-10-23 21:24:32 +00:00
private delegate int FormatFriendListNameDelegate(long a1, long a2, long a3, int a4, IntPtr data, long a6);
2020-10-23 21:24:32 +00:00
private delegate IntPtr OnReceiveFriendListChunkDelegate(IntPtr a1, IntPtr data);
private delegate IntPtr GetColourInfoDelegate(IntPtr handler, uint lookupResult);
2020-10-23 21:24:32 +00:00
2021-01-07 21:26:27 +00:00
private delegate byte ChatChannelChangeDelegate(IntPtr a1, uint channel);
2020-10-23 21:24:32 +00:00
2021-01-07 21:26:27 +00:00
private readonly Hook<RequestFriendListDelegate>? _friendListHook;
private readonly Hook<FormatFriendListNameDelegate>? _formatHook;
private readonly Hook<OnReceiveFriendListChunkDelegate>? _receiveChunkHook;
private readonly Hook<ChatChannelChangeDelegate>? _chatChannelChangeHook;
2020-10-23 21:24:32 +00:00
2021-01-07 21:26:27 +00:00
private readonly GetUiModuleDelegate? _getUiModule;
private readonly EasierProcessChatBoxDelegate? _easierProcessChatBox;
private readonly GetColourInfoDelegate? _getColourInfo;
private IntPtr UiModulePtr { get; }
private IntPtr ColourHandler { get; }
private IntPtr ColourLookup { get; }
private IntPtr _friendListManager = IntPtr.Zero;
private IntPtr _chatManager = IntPtr.Zero;
2020-10-23 21:24:32 +00:00
public bool RequestingFriendList { get; private set; }
2021-01-07 21:26:27 +00:00
private readonly List<Player> _friends = new List<Player>();
2020-10-23 21:24:32 +00:00
public delegate void ReceiveFriendListHandler(List<Player> friends);
public event ReceiveFriendListHandler? ReceiveFriendList;
2020-10-23 21:24:32 +00:00
public GameFunctions(Plugin plugin) {
2021-01-07 21:26:27 +00:00
this.Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin), "Plugin cannot be null");
var getUiModulePtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? 48 83 7F ?? 00 48 8B F0");
var easierProcessChatBoxPtr = this.Plugin.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9");
var friendListPtr = this.Plugin.ScanText("40 53 48 81 EC 80 0F 00 00 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B D9 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 44 0F B6 43 ?? 33 C9");
var formatPtr = this.Plugin.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 41 56 48 83 EC 30 48 8B 6C 24 ??");
var recvChunkPtr = this.Plugin.ScanText("48 89 5C 24 ?? 56 48 83 EC 20 48 8B 0D ?? ?? ?? ?? 48 8B F2");
var getColourPtr = this.Plugin.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B F2 48 8D B9 ?? ?? ?? ??");
var channelPtr = this.Plugin.ScanText("40 55 48 8D 6C 24 ?? 48 81 EC A0 00 00 00 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 45 ?? 48 8B 0D ?? ?? ?? ?? 33 C0 48 83 C1 10 89 45 ?? C7 45 ?? 01 00 00 00");
this.UiModulePtr = this.Plugin.GetStaticAddressFromSig("48 8B 0D ?? ?? ?? ?? 48 8D 54 24 ?? 48 83 C1 10 E8 ?? ?? ?? ??");
if (this.UiModulePtr == IntPtr.Zero) {
PluginLog.Warning("Static pointer was null: {}", nameof(this.UiModulePtr));
}
this.ColourHandler = this.Plugin.GetStaticAddressFromSig("48 8B 05 ?? ?? ?? ?? 48 8B A8 ?? ?? ?? ?? 48 85 ED 0F 84 ?? ?? ?? ??");
if (this.ColourHandler == IntPtr.Zero) {
PluginLog.Warning("Static pointer was null: {}", nameof(this.ColourHandler));
}
this.ColourLookup = this.Plugin.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? 8B 14 ?? 85 D2 7E ?? 48 8B 0D ?? ?? ?? ?? 48 83 C1 10 E8 ?? ?? ?? ?? 8B 70 ?? 41 8D 4D ??");
if (this.ColourLookup == IntPtr.Zero) {
PluginLog.Warning("Static pointer was null: {}", nameof(this.ColourLookup));
}
if (getUiModulePtr != IntPtr.Zero) {
this._getUiModule = Marshal.GetDelegateForFunctionPointer<GetUiModuleDelegate>(getUiModulePtr);
} else {
PluginLog.Warning("Pointer was null, disabling function: {}", nameof(getUiModulePtr));
}
if (easierProcessChatBoxPtr != IntPtr.Zero) {
this._easierProcessChatBox = Marshal.GetDelegateForFunctionPointer<EasierProcessChatBoxDelegate>(easierProcessChatBoxPtr);
} else {
PluginLog.Warning("Pointer was null, disabling function: {}", nameof(easierProcessChatBoxPtr));
}
if (getColourPtr != IntPtr.Zero) {
this._getColourInfo = Marshal.GetDelegateForFunctionPointer<GetColourInfoDelegate>(getColourPtr);
} else {
PluginLog.Warning("Pointer was null, disabling function: {}", nameof(getColourPtr));
}
if (friendListPtr != IntPtr.Zero) {
this._friendListHook = new Hook<RequestFriendListDelegate>(friendListPtr, new RequestFriendListDelegate(this.OnRequestFriendList));
} else {
PluginLog.Warning("Pointer was null, disabling hook: {}", nameof(friendListPtr));
}
if (formatPtr != IntPtr.Zero) {
this._formatHook = new Hook<FormatFriendListNameDelegate>(formatPtr, new FormatFriendListNameDelegate(this.OnFormatFriendList));
} else {
PluginLog.Warning("Pointer was null, disabling hook: {}", nameof(formatPtr));
}
if (recvChunkPtr != IntPtr.Zero) {
this._receiveChunkHook = new Hook<OnReceiveFriendListChunkDelegate>(recvChunkPtr, new OnReceiveFriendListChunkDelegate(this.OnReceiveFriendList));
} else {
PluginLog.Warning("Pointer was null, disabling hook: {}", nameof(recvChunkPtr));
}
if (channelPtr != IntPtr.Zero) {
this._chatChannelChangeHook = new Hook<ChatChannelChangeDelegate>(channelPtr, new ChatChannelChangeDelegate(this.ChangeChatChannelDetour));
} else {
PluginLog.Warning("Pointer was null, disabling hook: {}", nameof(channelPtr));
}
this._friendListHook?.Enable();
this._formatHook?.Enable();
this._receiveChunkHook?.Enable();
this._chatChannelChangeHook?.Enable();
2020-10-23 21:24:32 +00:00
}
public void ChangeChatChannel(InputChannel channel) {
if (this._chatManager == IntPtr.Zero || this._chatChannelChangeHook == null) {
return;
}
Marshal.WriteInt32(this._chatManager + ChannelOffset, (int) channel);
this._chatChannelChangeHook.Original(this._chatManager, (uint) channel);
}
// This function looks up a channel's user-defined colour.
//
// If this function would ever return 0, it returns null instead.
public uint? GetChannelColour(ChatCode channel) {
2021-01-07 21:26:27 +00:00
if (this.ColourLookup == IntPtr.Zero || this.ColourHandler == IntPtr.Zero) {
return null;
}
// Colours are retrieved by looking up their code in a lookup table. Some codes share a colour, so they're lumped into a parent code here.
// Only codes >= 10 (say) have configurable colours.
// After getting the lookup value for the code, it is passed into a function with a handler which returns a pointer.
// This pointer + 32 is the RGB value. This functions returns RGBA with A always max.
var parent = channel.Parent();
switch (parent) {
case ChatType.Debug:
case ChatType.Urgent:
case ChatType.Notice:
return channel.DefaultColour();
}
2021-01-07 21:26:27 +00:00
var lookupResult = (uint) Marshal.ReadInt32(this.ColourLookup, (int) parent * 4);
var info = this._getColourInfo(Marshal.ReadIntPtr(this.ColourHandler) + 16, lookupResult);
var rgb = (uint) Marshal.ReadInt32(info, 32) & 0xFFFFFF;
if (rgb == 0) {
return null;
}
return 0xFF | (rgb << 8);
}
2020-10-23 21:24:32 +00:00
public void ProcessChatBox(string message) {
2021-01-07 21:26:27 +00:00
if (this._easierProcessChatBox == null || this.UiModulePtr == IntPtr.Zero) {
return;
}
var uiModule = this._getUiModule(Marshal.ReadIntPtr(this.UiModulePtr));
2020-10-23 21:24:32 +00:00
if (uiModule == IntPtr.Zero) {
2021-01-07 21:26:27 +00:00
throw new ArgumentException("pointer was null", nameof(uiModule));
2020-10-23 21:24:32 +00:00
}
using var payload = new ChatPayload(message);
2021-01-07 21:26:27 +00:00
var mem1 = Marshal.AllocHGlobal(400);
Marshal.StructureToPtr(payload, mem1, false);
2020-10-23 21:24:32 +00:00
2021-01-07 21:26:27 +00:00
this._easierProcessChatBox(uiModule, mem1, IntPtr.Zero, 0);
2020-10-23 21:24:32 +00:00
Marshal.FreeHGlobal(mem1);
2020-10-23 21:24:32 +00:00
}
public bool RequestFriendList() {
2021-01-07 21:26:27 +00:00
if (this._friendListManager == IntPtr.Zero || this._friendListHook == null) {
2020-10-23 21:24:32 +00:00
return false;
}
this.RequestingFriendList = true;
2021-01-07 21:26:27 +00:00
this._friendListHook.Original(this._friendListManager);
2020-10-23 21:24:32 +00:00
return true;
}
2021-01-07 21:26:27 +00:00
private byte ChangeChatChannelDetour(IntPtr a1, uint channel) {
this._chatManager = a1;
2021-01-07 21:26:27 +00:00
// a1 + 0xfd0 is the chat channel byte (including for when clicking on shout)
this.Plugin.Server.OnChatChannelChange(channel);
return this._chatChannelChangeHook!.Original(a1, channel);
}
2020-10-23 21:24:32 +00:00
private byte OnRequestFriendList(IntPtr manager) {
2021-01-07 21:26:27 +00:00
this._friendListManager = manager;
// NOTE: if this is being called, hook isn't null
return this._friendListHook!.Original(manager);
2020-10-23 21:24:32 +00:00
}
private int OnFormatFriendList(long a1, long a2, long a3, int a4, IntPtr data, long a6) {
// have to call this first to populate cross-world info
2021-01-07 21:26:27 +00:00
// NOTE: if this is being called, hook isn't null
var ret = this._formatHook!.Original(a1, a2, a3, a4, data, a6);
2020-10-23 21:24:32 +00:00
if (!this.RequestingFriendList) {
return ret;
}
var entry = Marshal.PtrToStructure<FriendListEntryRaw>(data);
string? jobName = null;
2020-10-23 21:24:32 +00:00
if (entry.job > 0) {
2021-01-07 21:26:27 +00:00
jobName = this.Plugin.Interface.Data.GetExcelSheet<ClassJob>().GetRow(entry.job)?.Name;
2020-10-23 21:24:32 +00:00
}
2020-12-09 04:26:26 +00:00
// FIXME: remove this try/catch when lumina fixes bug with .Value
string? territoryName;
try {
2021-01-07 21:26:27 +00:00
territoryName = this.Plugin.Interface.Data.GetExcelSheet<TerritoryType>().GetRow(entry.territoryId)?.PlaceName?.Value?.Name;
2020-12-09 04:26:26 +00:00
} catch (NullReferenceException) {
territoryName = null;
}
2020-10-23 21:24:32 +00:00
var player = new Player {
Name = entry.Name(),
FreeCompany = entry.FreeCompany(),
Status = entry.flags,
CurrentWorld = entry.currentWorldId,
2021-01-07 21:26:27 +00:00
CurrentWorldName = this.Plugin.Interface.Data.GetExcelSheet<World>().GetRow(entry.currentWorldId)?.Name,
2020-10-23 21:24:32 +00:00
HomeWorld = entry.homeWorldId,
2021-01-07 21:26:27 +00:00
HomeWorldName = this.Plugin.Interface.Data.GetExcelSheet<World>().GetRow(entry.homeWorldId)?.Name,
2020-10-23 21:24:32 +00:00
Territory = entry.territoryId,
2020-12-09 04:26:26 +00:00
TerritoryName = territoryName,
2020-10-23 21:24:32 +00:00
Job = entry.job,
JobName = jobName,
GrandCompany = entry.grandCompany,
2021-01-07 21:26:27 +00:00
GrandCompanyName = this.Plugin.Interface.Data.GetExcelSheet<GrandCompany>().GetRow(entry.grandCompany)?.Name,
2020-10-23 21:24:32 +00:00
Languages = entry.langsEnabled,
MainLanguage = entry.mainLanguage,
};
2021-01-07 21:26:27 +00:00
this._friends.Add(player);
2020-10-23 21:24:32 +00:00
return ret;
}
private IntPtr OnReceiveFriendList(IntPtr a1, IntPtr data) {
2021-01-07 21:26:27 +00:00
// NOTE: if this is being called, hook isn't null
var ret = this._receiveChunkHook!.Original(a1, data);
2020-10-23 21:24:32 +00:00
// + 0xc
// 1 = party
// 2 = friends
// 3 = linkshell
// doesn't run (though same memory gets updated) for cwl or blacklist
// + 0x8 is current number of results returned or 0 when end of list
if (!this.RequestingFriendList) {
goto Return;
}
if (Marshal.ReadByte(data + 0xc) != 2 || Marshal.ReadInt16(data + 0x8) != 0) {
goto Return;
}
2021-01-07 21:26:27 +00:00
this.ReceiveFriendList?.Invoke(this._friends);
this._friends.Clear();
this.RequestingFriendList = false;
2020-10-23 21:24:32 +00:00
Return:
2020-10-23 21:24:32 +00:00
return ret;
}
public void Dispose() {
2021-01-07 21:26:27 +00:00
this._friendListHook?.Dispose();
this._formatHook?.Dispose();
this._receiveChunkHook?.Dispose();
this._chatChannelChangeHook?.Dispose();
2020-10-23 21:24:32 +00:00
}
}
[StructLayout(LayoutKind.Explicit)]
[SuppressMessage("ReSharper", "PrivateFieldCanBeConvertedToLocalVariable")]
readonly struct ChatPayload : IDisposable {
[FieldOffset(0)]
readonly IntPtr textPtr;
2020-10-23 21:24:32 +00:00
[FieldOffset(16)]
readonly ulong textLen;
[FieldOffset(8)]
readonly ulong unk1;
[FieldOffset(24)]
readonly ulong unk2;
2020-10-23 21:24:32 +00:00
internal ChatPayload(string text) {
byte[] stringBytes = Encoding.UTF8.GetBytes(text);
this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length);
Marshal.WriteByte(this.textPtr + stringBytes.Length, 0);
2021-01-07 21:26:27 +00:00
this.textLen = (ulong) (stringBytes.Length + 1);
2020-10-23 21:24:32 +00:00
this.unk1 = 64;
this.unk2 = 0;
}
public void Dispose() {
Marshal.FreeHGlobal(this.textPtr);
}
}
[StructLayout(LayoutKind.Sequential)]
struct FriendListEntryRaw {
readonly ulong unk1;
internal readonly ulong flags;
2020-10-23 21:24:32 +00:00
readonly uint unk2;
2020-10-23 21:24:32 +00:00
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
readonly byte[] unk3;
2020-10-23 21:24:32 +00:00
internal readonly ushort currentWorldId;
internal readonly ushort homeWorldId;
internal readonly ushort territoryId;
internal readonly byte grandCompany;
internal readonly byte mainLanguage;
internal readonly byte langsEnabled;
2020-10-23 21:24:32 +00:00
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
readonly byte[] unk4;
2020-10-23 21:24:32 +00:00
internal readonly byte job;
2020-10-23 21:24:32 +00:00
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
readonly byte[] name;
2020-10-23 21:24:32 +00:00
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
readonly byte[] fc;
private static string? HandleString(IEnumerable<byte> bytes) {
2020-10-23 21:24:32 +00:00
byte[] nonNull = bytes.TakeWhile(b => b != 0).ToArray();
return nonNull.Length == 0 ? null : Encoding.UTF8.GetString(nonNull);
2020-10-23 21:24:32 +00:00
}
public string? Name() => HandleString(this.name);
public string? FreeCompany() => HandleString(this.fc);
2020-10-23 21:24:32 +00:00
}
}