From 471be104e10143836278cd8ba769b8116e8b1d82 Mon Sep 17 00:00:00 2001 From: Anna Date: Fri, 14 Jan 2022 13:25:33 -0500 Subject: [PATCH] feat: add reply in selected chat mode --- ChatTwo/Chunk.cs | 1 + ChatTwo/Code/ChatTypeExt.cs | 31 +++++++++++++- ChatTwo/GameFunctions/Chat.cs | 80 ++++++++++++++++++++++++++--------- ChatTwo/Message.cs | 4 ++ ChatTwo/PayloadHandler.cs | 26 +++++++----- ChatTwo/PluginUi.cs | 2 +- 6 files changed, 113 insertions(+), 31 deletions(-) diff --git a/ChatTwo/Chunk.cs b/ChatTwo/Chunk.cs index cab50ec..7c6a53b 100755 --- a/ChatTwo/Chunk.cs +++ b/ChatTwo/Chunk.cs @@ -4,6 +4,7 @@ using Dalamud.Game.Text.SeStringHandling; namespace ChatTwo; internal abstract class Chunk { + internal Message? Message { get; set; } internal SeString? Source { get; set; } internal Payload? Link { get; set; } diff --git a/ChatTwo/Code/ChatTypeExt.cs b/ChatTwo/Code/ChatTypeExt.cs index 5fefb5b..c69a2c4 100755 --- a/ChatTwo/Code/ChatTypeExt.cs +++ b/ChatTwo/Code/ChatTypeExt.cs @@ -3,7 +3,7 @@ using ChatTwo.Util; namespace ChatTwo.Code; internal static class ChatTypeExt { - internal static string? Name(this ChatType type) { + internal static string Name(this ChatType type) { return type switch { ChatType.Debug => "Debug", ChatType.Urgent => "Urgent", @@ -206,4 +206,33 @@ internal static class ChatTypeExt { return null; } } + + internal static InputChannel? ToInputChannel(this ChatType type) => type switch { + ChatType.TellOutgoing => InputChannel.Tell, + ChatType.Say => InputChannel.Say, + ChatType.Party => InputChannel.Party, + ChatType.Alliance => InputChannel.Alliance, + ChatType.Yell => InputChannel.Yell, + ChatType.Shout => InputChannel.Shout, + ChatType.FreeCompany => InputChannel.FreeCompany, + ChatType.PvpTeam => InputChannel.PvpTeam, + ChatType.NoviceNetwork => InputChannel.NoviceNetwork, + ChatType.CrossLinkshell1 => InputChannel.CrossLinkshell1, + ChatType.CrossLinkshell2 => InputChannel.CrossLinkshell2, + ChatType.CrossLinkshell3 => InputChannel.CrossLinkshell3, + ChatType.CrossLinkshell4 => InputChannel.CrossLinkshell4, + ChatType.CrossLinkshell5 => InputChannel.CrossLinkshell5, + ChatType.CrossLinkshell6 => InputChannel.CrossLinkshell6, + ChatType.CrossLinkshell7 => InputChannel.CrossLinkshell7, + ChatType.CrossLinkshell8 => InputChannel.CrossLinkshell8, + ChatType.Linkshell1 => InputChannel.Linkshell1, + ChatType.Linkshell2 => InputChannel.Linkshell2, + ChatType.Linkshell3 => InputChannel.Linkshell3, + ChatType.Linkshell4 => InputChannel.Linkshell4, + ChatType.Linkshell5 => InputChannel.Linkshell5, + ChatType.Linkshell6 => InputChannel.Linkshell6, + ChatType.Linkshell7 => InputChannel.Linkshell7, + ChatType.Linkshell8 => InputChannel.Linkshell8, + _ => null, + }; } diff --git a/ChatTwo/GameFunctions/Chat.cs b/ChatTwo/GameFunctions/Chat.cs index 1b5daa5..0ed31a8 100755 --- a/ChatTwo/GameFunctions/Chat.cs +++ b/ChatTwo/GameFunctions/Chat.cs @@ -12,20 +12,27 @@ using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Client.UI.Shell; using FFXIVClientStructs.FFXIV.Component.GUI; using Siggingway; +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; namespace ChatTwo.GameFunctions; internal sealed unsafe class Chat : IDisposable { + // Functions + [Signature("E8 ?? ?? ?? ?? 0F B7 44 37 ??", Fallibility = Fallibility.Fallible)] private readonly delegate* unmanaged _changeChatChannel = null!; [Signature("4C 8B 81 ?? ?? ?? ?? 4D 85 C0 74 17", Fallibility = Fallibility.Fallible)] private readonly delegate* unmanaged _getContentIdForChatEntry = null!; + // Hooks + private delegate byte ChatLogRefreshDelegate(IntPtr log, ushort eventId, AtkValue* value); private delegate IntPtr ChangeChannelNameDelegate(IntPtr agent); + private delegate void ReplyInSelectedChatModeDelegate(AgentInterface* agent); + [Signature( "40 53 56 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 49 8B F0 8B FA", DetourName = nameof(ChatLogRefreshDetour) @@ -38,6 +45,23 @@ internal sealed unsafe class Chat : IDisposable { )] private Hook? ChangeChannelNameHook { get; init; } + [Signature( + "48 89 5C 24 ?? 57 48 83 EC 30 8B B9 ?? ?? ?? ?? 48 8B D9 83 FF FE", + DetourName = nameof(ReplyInSelectedChatModeDetour) + )] + private Hook? ReplyInSelectedChatModeHook { get; init; } + + // Offsets + + #pragma warning disable 0649 + + [Signature("8B B9 ?? ?? ?? ?? 48 8B D9 83 FF FE 0F 84", Offset = 2)] + private readonly int? _replyChannelOffset; + + #pragma warning restore 0649 + + // Events + internal delegate void ChatActivatedEventDelegate(string? input); internal event ChatActivatedEventDelegate? Activated; @@ -51,6 +75,7 @@ internal sealed unsafe class Chat : IDisposable { this.ChatLogRefreshHook?.Enable(); this.ChangeChannelNameHook?.Enable(); + this.ReplyInSelectedChatModeHook?.Enable(); this.Plugin.ClientState.Login += this.Login; this.Login(null, null); @@ -59,6 +84,7 @@ internal sealed unsafe class Chat : IDisposable { public void Dispose() { this.Plugin.ClientState.Login -= this.Login; + this.ReplyInSelectedChatModeHook?.Dispose(); this.ChangeChannelNameHook?.Dispose(); this.ChatLogRefreshHook?.Dispose(); @@ -127,27 +153,43 @@ internal sealed unsafe class Chat : IDisposable { } private byte ChatLogRefreshDetour(IntPtr log, ushort eventId, AtkValue* value) { - if (eventId == 0x31 && value != null && value->UInt is 0x05 or 0x0C) { - string? eventInput = null; - - var str = value + 2; - if (str != null && str->String != null) { - var input = MemoryHelper.ReadStringNullTerminated((IntPtr) str->String); - if (input.Length > 0) { - eventInput = input; - } - } - - try { - this.Activated?.Invoke(eventInput); - } catch (Exception ex) { - PluginLog.LogError(ex, "Error in ChatActivated event"); - } - - return 0; + if (eventId != 0x31 || value == null || value->UInt is not (0x05 or 0x0C)) { + return this.ChatLogRefreshHook!.Original(log, eventId, value); } - return this.ChatLogRefreshHook!.Original(log, eventId, value); + string? eventInput = null; + + var str = value + 2; + if (str != null && ((int) str->Type & 0xF) == (int) ValueType.String && str->String != null) { + var input = MemoryHelper.ReadStringNullTerminated((IntPtr) str->String); + if (input.Length > 0) { + eventInput = input; + } + } + + try { + this.Activated?.Invoke(eventInput); + } catch (Exception ex) { + PluginLog.LogError(ex, "Error in ChatActivated event"); + } + + return 0; + } + + private void ReplyInSelectedChatModeDetour(AgentInterface* agent) { + if (this._replyChannelOffset == null) { + goto Original; + } + + var replyMode = *(int*) ((IntPtr) agent + this._replyChannelOffset.Value); + if (replyMode == -2) { + goto Original; + } + + this.SetChannel((InputChannel) replyMode); + + Original: + this.ReplyInSelectedChatModeHook!.Original(agent); } internal ulong? GetContentIdForEntry(uint index) { diff --git a/ChatTwo/Message.cs b/ChatTwo/Message.cs index 4b21d9f..b03329e 100755 --- a/ChatTwo/Message.cs +++ b/ChatTwo/Message.cs @@ -15,5 +15,9 @@ internal class Message { this.Code = code; this.Sender = sender; this.Content = content; + + foreach (var chunk in sender.Concat(content)) { + chunk.Message = this; + } } } diff --git a/ChatTwo/PayloadHandler.cs b/ChatTwo/PayloadHandler.cs index 5ecb6a2..b5fb503 100755 --- a/ChatTwo/PayloadHandler.cs +++ b/ChatTwo/PayloadHandler.cs @@ -1,5 +1,6 @@ using System.Numerics; using System.Reflection; +using ChatTwo.Code; using ChatTwo.Ui; using ChatTwo.Util; using Dalamud.Game.ClientState.Objects.SubKinds; @@ -16,7 +17,7 @@ internal sealed class PayloadHandler { private PluginUi Ui { get; } private ChatLog Log { get; } - private HashSet PopupPayloads { get; set; } = new(); + private HashSet<(Chunk, Payload)> PopupPayloads { get; set; } = new(); private bool _handleTooltips; private uint _hoveredItem; @@ -40,8 +41,8 @@ internal sealed class PayloadHandler { } private void DrawPopups() { - var newPopups = new HashSet(); - foreach (var payload in this.PopupPayloads) { + var newPopups = new HashSet<(Chunk, Payload)>(); + foreach (var (chunk, payload) in this.PopupPayloads) { var id = PopupId(payload); if (id == null) { continue; @@ -51,12 +52,12 @@ internal sealed class PayloadHandler { continue; } - newPopups.Add(payload); + newPopups.Add((chunk, payload)); ImGui.PushID(id); switch (payload) { case PlayerPayload player: { - this.DrawPlayerPopup(player); + this.DrawPlayerPopup(chunk, player); break; } case ItemPayload item: { @@ -78,7 +79,7 @@ internal sealed class PayloadHandler { this.LeftClickPayload(chunk, payload); break; case ImGuiMouseButton.Right: - this.RightClickPayload(payload); + this.RightClickPayload(chunk, payload); break; } } @@ -211,11 +212,11 @@ internal sealed class PayloadHandler { } } - private void RightClickPayload(Payload payload) { + private void RightClickPayload(Chunk chunk, Payload payload) { switch (payload) { case PlayerPayload: case ItemPayload: { - this.PopupPayloads.Add(payload); + this.PopupPayloads.Add((chunk, payload)); ImGui.OpenPopup(PopupId(payload)); break; } @@ -267,7 +268,7 @@ internal sealed class PayloadHandler { } } - private void DrawPlayerPopup(PlayerPayload player) { + private void DrawPlayerPopup(Chunk chunk, PlayerPayload player) { var name = player.PlayerName; if (player.World.IsPublic) { name += $"{player.World.Name}"; @@ -313,6 +314,12 @@ internal sealed class PayloadHandler { } } + var inputChannel = chunk.Message?.Code.Type.ToInputChannel(); + if (inputChannel != null && ImGui.Selectable("Reply in Selected Chat Mode")) { + this.Ui.Plugin.Functions.Chat.SetChannel(inputChannel.Value); + this.Log.Activate = true; + } + if (ImGui.Selectable("Target") && this.FindCharacterForPayload(player) is { } obj) { this.Ui.Plugin.TargetManager.SetTarget(obj); } @@ -323,7 +330,6 @@ internal sealed class PayloadHandler { // Add to Blacklist 0x1C // View Party Finder 0x2E - // Reply in Selected Chat Mode 0x64 } private PlayerCharacter? FindCharacterForPayload(PlayerPayload payload) { diff --git a/ChatTwo/PluginUi.cs b/ChatTwo/PluginUi.cs index 5e316d0..55c34f3 100755 --- a/ChatTwo/PluginUi.cs +++ b/ChatTwo/PluginUi.cs @@ -26,7 +26,7 @@ internal sealed class PluginUi : IDisposable { private (GCHandle, int) _jpFont; private (GCHandle, int) _gameSymFont; - private ImVector _ranges; + private readonly ImVector _ranges; private GCHandle _jpRange = GCHandle.Alloc( GlyphRangesJapanese.GlyphRanges,