diff --git a/XIVChatPlugin/Client.cs b/XIVChatPlugin/Client.cs index b561295..ceaf9ce 100644 --- a/XIVChatPlugin/Client.cs +++ b/XIVChatPlugin/Client.cs @@ -26,6 +26,8 @@ namespace XIVChatPlugin { internal Channel Queue { get; } = Channel.CreateUnbounded(); + internal uint BacklogSequence { get; set; } + internal void Disconnect() { this.Connected = false; this.TokenSource.Cancel(); diff --git a/XIVChatPlugin/GameFunctions.cs b/XIVChatPlugin/GameFunctions.cs index 0dd249b..55fa47b 100644 --- a/XIVChatPlugin/GameFunctions.cs +++ b/XIVChatPlugin/GameFunctions.cs @@ -6,11 +6,35 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; using System.Text; +using Dalamud.Game.Text.SeStringHandling; using Dalamud.Logging; +using Dalamud.Memory; using XIVChatCommon.Message; +using XIVChatCommon.Message.Server; namespace XIVChatPlugin { internal class GameFunctions : IDisposable { + private static class Signatures { + internal const string GetUiModule = "E8 ?? ?? ?? ?? 48 83 7F ?? 00 48 8B F0"; + internal const string ProcessChat = "48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 45 84 C9"; + internal const string Input = "80 B9 ?? ?? ?? ?? ?? 0F 9C C0"; + internal const string InputAfk = "E8 ?? ?? ?? ?? 0F 28 74 24 ?? 0F B6 F0"; + internal const string FriendList = "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"; + internal const string Format = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 41 56 48 83 EC 30 48 8B 6C 24"; + internal const string ReceiveChunk = "48 89 5C 24 ?? 56 48 83 EC 20 48 8B 0D ?? ?? ?? ?? 48 8B F2"; + + internal const string GetColour = "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 8B F2 48 8D B9"; + + internal const string Channel = "E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 85 D2 BB"; + internal const string ChannelCommand = "E8 ?? ?? ?? ?? 0F B7 44 37"; + internal const string ChannelNameChange = "E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8D 4D B0 48 8B F8 E8 ?? ?? ?? ?? 41 8B D6"; + internal const string XivStringCtor = "E8 ?? ?? ?? ?? 44 2B F7"; + internal const string XivStringDtor = "E8 ?? ?? ?? ?? B0 6E"; + internal const string UiModule = "48 8B 0D ?? ?? ?? ?? 48 8D 54 24 ?? 48 83 C1 10 E8"; + internal const string ColourHandler = "48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 8B 0D ?? ?? ?? ?? 48 85 C9 0F 84 ?? ?? ?? ?? 66 85 DB 0F 94 C2 E8 ?? ?? ?? ?? E9"; + internal const string ColourLookup = "48 8D 0D ?? ?? ?? ?? 8B 14 ?? 85 D2 7E ?? 48 8B 0D ?? ?? ?? ?? 48 83 C1 10 E8 ?? ?? ?? ?? 8B 70 ?? 41 8D 4D"; + } + private Plugin Plugin { get; } private delegate IntPtr GetUiModuleDelegate(IntPtr basePtr); @@ -31,6 +55,8 @@ namespace XIVChatPlugin { private delegate byte ChatChannelChangeDelegate(IntPtr a1, uint channel); + private delegate IntPtr ChatChannelChangeNameDelegate(IntPtr a1); + private delegate IntPtr ChannelChangeCommandDelegate(IntPtr a1, int inputChannel, uint linkshellIdx, IntPtr tellTarget, char canChangeChannel); private delegate IntPtr XivStringCtorDelegate(IntPtr memory); @@ -43,6 +69,7 @@ namespace XIVChatPlugin { private readonly Hook? _formatHook; private readonly Hook? _receiveChunkHook; private readonly Hook? _chatChannelChangeHook; + private readonly Hook? _chatChannelChangeNameHook; private readonly GetUiModuleDelegate? _getUiModule; private readonly EasierProcessChatBoxDelegate? _easierProcessChatBox; @@ -51,6 +78,22 @@ namespace XIVChatPlugin { private readonly XivStringCtorDelegate? _xivStringCtor; private readonly XivStringDtorDelegate? _xivStringDtor; + public ServerHousingLocation HousingLocation { + get { + var info = this.Plugin.Common.Functions.Housing.Location; + if (info == null) { + return new ServerHousingLocation(null, null, false, null); + } + + var ward = info.Ward; + var plot = info.Plot ?? info.Yard ?? info.Apartment; + var wing = (byte?) info.ApartmentWing; + var exterior = info.Yard != null; + + return new ServerHousingLocation(ward, plot, exterior, wing); + } + } + [Flags] private enum InputSetters { None = 0, @@ -77,30 +120,31 @@ namespace XIVChatPlugin { internal GameFunctions(Plugin plugin) { this.Plugin = plugin; - 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 inputPtr = this.Plugin.ScanText("80 B9 ?? ?? ?? ?? ?? 0F 9C C0"); - var inputAfkPtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? 0F 28 74 24 ?? 0F B6 F0"); - 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("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 85 D2 BB ?? ?? ?? ??"); - var channelCommandPtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? 0F B7 44 37 ??"); - var xivStringCtorPtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? 44 2B F7"); - var xivStringDtorPtr = this.Plugin.ScanText("E8 ?? ?? ?? ?? B0 6E"); + var getUiModulePtr = this.Plugin.ScanText(Signatures.GetUiModule); + var easierProcessChatBoxPtr = this.Plugin.ScanText(Signatures.ProcessChat); + var inputPtr = this.Plugin.ScanText(Signatures.Input); + var inputAfkPtr = this.Plugin.ScanText(Signatures.InputAfk); + var friendListPtr = this.Plugin.ScanText(Signatures.FriendList); + var formatPtr = this.Plugin.ScanText(Signatures.Format); + var recvChunkPtr = this.Plugin.ScanText(Signatures.ReceiveChunk); + var getColourPtr = this.Plugin.ScanText(Signatures.GetColour); + var channelPtr = this.Plugin.ScanText(Signatures.Channel); + var channelNamePtr = this.Plugin.ScanText(Signatures.ChannelNameChange); + var channelCommandPtr = this.Plugin.ScanText(Signatures.ChannelCommand); + var xivStringCtorPtr = this.Plugin.ScanText(Signatures.XivStringCtor); + var xivStringDtorPtr = this.Plugin.ScanText(Signatures.XivStringDtor); - this.UiModulePtr = this.Plugin.GetStaticAddressFromSig("48 8B 0D ?? ?? ?? ?? 48 8D 54 24 ?? 48 83 C1 10 E8 ?? ?? ?? ??"); + this.UiModulePtr = this.Plugin.GetStaticAddressFromSig(Signatures.UiModule); if (this.UiModulePtr == IntPtr.Zero) { PluginLog.Warning("Static pointer was null: {0}", nameof(this.UiModulePtr)); } - this.ColourHandler = this.Plugin.GetStaticAddressFromSig("48 8B 05 ?? ?? ?? ?? 48 8B A8 ?? ?? ?? ?? 48 85 ED 0F 84 ?? ?? ?? ??"); + this.ColourHandler = this.Plugin.GetStaticAddressFromSig(Signatures.ColourHandler); if (this.ColourHandler == IntPtr.Zero) { PluginLog.Warning("Static pointer was null: {0}", 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 ??"); + this.ColourLookup = this.Plugin.GetStaticAddressFromSig(Signatures.ColourLookup); if (this.ColourLookup == IntPtr.Zero) { PluginLog.Warning("Static pointer was null: {0}", nameof(this.ColourLookup)); } @@ -165,6 +209,12 @@ namespace XIVChatPlugin { PluginLog.Warning("Pointer was null, disabling hook: {0}", nameof(channelPtr)); } + if (channelNamePtr != IntPtr.Zero) { + this._chatChannelChangeNameHook = new Hook(channelNamePtr, this.ChangeChatChannelNameDetour); + } else { + PluginLog.Warning("Pointer was null, disabling hook: {0}", nameof(channelNamePtr)); + } + if (inputPtr != IntPtr.Zero) { this._isInputHook = new Hook(inputPtr, this.IsInputDetour); } else { @@ -181,6 +231,7 @@ namespace XIVChatPlugin { this._formatHook?.Enable(); this._receiveChunkHook?.Enable(); this._chatChannelChangeHook?.Enable(); + this._chatChannelChangeNameHook?.Enable(); this._isInputHook?.Enable(); this._isInputAfkHook?.Enable(); @@ -283,11 +334,45 @@ namespace XIVChatPlugin { private byte ChangeChatChannelDetour(IntPtr a1, uint channel) { this._chatManager = a1; + // Last ShB patch // a1 + 0xfd0 is the chat channel byte (including for when clicking on shout) - this.Plugin.Server.OnChatChannelChange(channel); return this._chatChannelChangeHook!.Original(a1, channel); } + private unsafe IntPtr ChangeChatChannelNameDetour(IntPtr a1) { + // Last ShB patch + // +0x40 = chat channel (byte or uint?) + // channel is 17 (maybe 18?) for tells + // +0x48 = pointer to channel name string + var ret = this._chatChannelChangeNameHook!.Original(a1); + if (a1 == IntPtr.Zero) { + return ret; + } + + var channel = *(uint*) (a1 + 0x40); + if (channel is 17 or 18) { + channel = 0; + } + + SeString? name = null; + var namePtrPtr = (byte**) (a1 + 0x48); + if (namePtrPtr != null) { + var namePtr = *namePtrPtr; + name = MemoryHelper.ReadSeStringNullTerminated((IntPtr) namePtr); + if (name.Payloads.Count == 0) { + name = null; + } + } + + if (name == null) { + return ret; + } + + this.Plugin.Server.OnChatChannelChange(channel, name); + + return ret; + } + private byte OnRequestFriendList(IntPtr manager) { this._friendListManager = manager; // NOTE: if this is being called, hook isn't null @@ -378,6 +463,7 @@ namespace XIVChatPlugin { this._formatHook?.Dispose(); this._receiveChunkHook?.Dispose(); this._chatChannelChangeHook?.Dispose(); + this._chatChannelChangeNameHook?.Dispose(); this._isInputHook?.Dispose(); this._isInputAfkHook?.Dispose(); diff --git a/XIVChatPlugin/Plugin.cs b/XIVChatPlugin/Plugin.cs index 1546415..5ea7432 100644 --- a/XIVChatPlugin/Plugin.cs +++ b/XIVChatPlugin/Plugin.cs @@ -10,6 +10,7 @@ using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Gui; using Dalamud.IoC; +using XivCommon; #if DEBUG using System.IO; #endif @@ -44,6 +45,7 @@ namespace XIVChatPlugin { [PluginService] private SigScanner SigScanner { get; init; } = null!; + internal XivCommonBase Common { get; } internal Configuration Config { get; } private PluginUi Ui { get; } internal Server Server { get; private set; } @@ -64,6 +66,7 @@ namespace XIVChatPlugin { } public Plugin() { + this.Common = new XivCommonBase(); this.Events = new InternalEvents(); // load libsodium.so from debug location if in debug mode @@ -118,10 +121,13 @@ namespace XIVChatPlugin { this.ClientState.Logout -= this.Server.OnLogOut; this.ClientState.TerritoryChanged -= this.Server.OnTerritoryChange; this.CommandManager.RemoveHandler("/xivchat"); + this.Functions.Dispose(); foreach (var ipc in this.Ipcs) { ipc.Dispose(); } + + this.Common.Dispose(); } internal void StartRelay() { diff --git a/XIVChatPlugin/Server.cs b/XIVChatPlugin/Server.cs index a482dc1..39f7204 100644 --- a/XIVChatPlugin/Server.cs +++ b/XIVChatPlugin/Server.cs @@ -59,11 +59,15 @@ namespace XIVChatPlugin { private bool _sendPlayerData; private readonly ConcurrentQueue _awaitingPlayerData = new(); private readonly ConcurrentQueue _awaitingAvailability = new(); + private readonly ConcurrentQueue _awaitingHousingLocation = new(); private volatile bool _running; private bool Running => this._running; private InputChannel _currentChannel = InputChannel.Say; + private SeString? _currentChannelName; + + private ServerHousingLocation _lastHousingLocation; private const int MaxMessageSize = 128_000; @@ -73,6 +77,8 @@ namespace XIVChatPlugin { this.RegenerateKeyPair(); } + this._lastHousingLocation = this._plugin.Functions.HousingLocation; + this._sendWatch.Start(); this._plugin.Functions.ReceiveFriendList += this.OnReceiveFriendList; @@ -251,6 +257,12 @@ namespace XIVChatPlugin { this._sendPlayerData = false; } + var housingLocation = this._plugin.Functions.HousingLocation; + if (!Equals(housingLocation, this._lastHousingLocation)) { + this.BroadcastMessage(housingLocation, ClientPreference.HousingLocationSupport); + this._lastHousingLocation = housingLocation; + } + while (this._awaitingPlayerData.TryDequeue(out var id)) { if (!this.Clients.TryGetValue(id, out var client)) { continue; @@ -269,6 +281,14 @@ namespace XIVChatPlugin { client.Queue.Writer.TryWrite(new Availability(available)); } + while (this._awaitingHousingLocation.TryDequeue(out var id)) { + if (!this.Clients.TryGetValue(id, out var client) || client.Handshake == null) { + continue; + } + + client.Queue.Writer.TryWrite(this._lastHousingLocation); + } + int time; if (this._toGame.TryPeek(out var peek) && PublicPrefixes.Any(prefix => peek.StartsWith(prefix))) { time = 1_000; @@ -358,7 +378,7 @@ namespace XIVChatPlugin { handshake.Keys.tx, new ServerChannel( channel, - this.LocalisedChannelName(channel) + this._currentChannelName?.TextValue ?? this.LocalisedChannelName(channel) ), this._tokenSource.Token ); @@ -491,6 +511,11 @@ namespace XIVChatPlugin { var preferences = ClientPreferences.Decode(payload); client.Preferences = preferences; + // immediately queue housing location + if (client.GetPreference(ClientPreference.HousingLocationSupport, false)) { + this._awaitingHousingLocation.Enqueue(id); + } + break; case ClientOperation.Channel: var channel = ClientChannel.Decode(payload); @@ -574,7 +599,7 @@ namespace XIVChatPlugin { var responseMessages = new List(); async Task SendBacklog() { - var resp = new ServerBacklog(responseMessages.ToArray()); + var resp = new ServerBacklog(responseMessages.ToArray(), ++client.BacklogSequence); try { await client.Queue.Writer.WriteAsync(resp); } catch (Exception ex) { @@ -716,6 +741,14 @@ namespace XIVChatPlugin { } } + private void BroadcastMessage(Encodable message, ClientPreference preference) { + foreach (var client in this.Clients.Values) { + if (client.GetPreference(preference, false)) { + client.Queue.Writer.TryWrite(message); + } + } + } + private string LocalisedChannelName(InputChannel channel) { uint rowId = channel switch { InputChannel.Tell => 3, @@ -749,13 +782,24 @@ namespace XIVChatPlugin { return this._plugin.DataManager.GetExcelSheet()!.GetRow(rowId)?.Name ?? string.Empty; } - internal void OnChatChannelChange(uint channel) { + internal void OnChatChannelChange(uint channel, SeString name) { + // for now, to avoid changing the protocol further, convert crossworld icon into font icon + for (var i = 0; i < name.Payloads.Count; i++) { + var payload = name.Payloads[i]; + if (payload is IconPayload { Icon: BitmapFontIcon.CrossWorld }) { + name.Payloads[i] = new TextPayload("\ue05d"); + } + } + var inputChannel = (InputChannel) channel; + if (inputChannel == this._currentChannel && name.Encode().SequenceEqual(this._currentChannelName?.Encode() ?? Array.Empty())) { + return; + } + this._currentChannel = inputChannel; + this._currentChannelName = name; - var localisedName = this.LocalisedChannelName(inputChannel); - - var msg = new ServerChannel(inputChannel, localisedName); + var msg = new ServerChannel(inputChannel, name.TextValue); this.BroadcastMessage(msg); } diff --git a/XIVChatPlugin/Util.cs b/XIVChatPlugin/Util.cs index 22d8641..cd026b2 100644 --- a/XIVChatPlugin/Util.cs +++ b/XIVChatPlugin/Util.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Runtime.InteropServices; namespace XIVChatPlugin { internal static class Util { @@ -70,5 +71,23 @@ namespace XIVChatPlugin { return bytes.ToArray(); } + + internal static IntPtr FollowPointerChain(IntPtr start, IEnumerable offsets) { + if (start == IntPtr.Zero) { + return IntPtr.Zero; + } + + // PluginLog.Log($"start: {start.ToInt64():x}"); + + foreach (var offset in offsets) { + start = Marshal.ReadIntPtr(start + offset); + // PluginLog.Log($" + {offset}: {start.ToInt64():x}"); + if (start == IntPtr.Zero) { + return IntPtr.Zero; + } + } + + return start; + } } } diff --git a/XIVChatPlugin/XIVChatPlugin.csproj b/XIVChatPlugin/XIVChatPlugin.csproj index 964471d..7be2285 100644 --- a/XIVChatPlugin/XIVChatPlugin.csproj +++ b/XIVChatPlugin/XIVChatPlugin.csproj @@ -32,11 +32,13 @@ - - + + + - + +