From c2b83116f2bf98cc9cfbe09427886ff64ce4399b Mon Sep 17 00:00:00 2001 From: Anna Clemens Date: Fri, 28 Jan 2022 23:01:10 -0500 Subject: [PATCH] feat: start adding our own shortcut support --- ChatTwo/GameFunctions/Chat.cs | 335 +++++++++++++++--- .../GameFunctions/Types/ChannelSwitchInfo.cs | 17 + ChatTwo/GameFunctions/Types/Keybind.cs | 11 + ChatTwo/GameFunctions/Types/ModifierFlag.cs | 8 + ChatTwo/GameFunctions/Types/RotateMode.cs | 7 + .../GameFunctions/Types/TellHistoryInfo.cs | 13 + ChatTwo/GameFunctions/Types/TellReason.cs | 7 + ChatTwo/PayloadHandler.cs | 8 + ChatTwo/Plugin.cs | 4 + ChatTwo/Ui/ChatLog.cs | 97 ++++- ChatTwo/Util/NumUtil.cs | 9 + 11 files changed, 458 insertions(+), 58 deletions(-) create mode 100755 ChatTwo/GameFunctions/Types/ChannelSwitchInfo.cs create mode 100755 ChatTwo/GameFunctions/Types/Keybind.cs create mode 100755 ChatTwo/GameFunctions/Types/ModifierFlag.cs create mode 100755 ChatTwo/GameFunctions/Types/RotateMode.cs create mode 100755 ChatTwo/GameFunctions/Types/TellHistoryInfo.cs create mode 100755 ChatTwo/GameFunctions/Types/TellReason.cs create mode 100755 ChatTwo/Util/NumUtil.cs diff --git a/ChatTwo/GameFunctions/Chat.cs b/ChatTwo/GameFunctions/Chat.cs index adca8ec..b2e7f43 100755 --- a/ChatTwo/GameFunctions/Chat.cs +++ b/ChatTwo/GameFunctions/Chat.cs @@ -1,6 +1,8 @@ using System.Text; using ChatTwo.Code; +using ChatTwo.GameFunctions.Types; using ChatTwo.Util; +using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; using Dalamud.Logging; @@ -25,6 +27,15 @@ internal sealed unsafe class Chat : IDisposable { [Signature("4C 8B 81 ?? ?? ?? ?? 4D 85 C0 74 17", Fallibility = Fallibility.Fallible)] private readonly delegate* unmanaged _getContentIdForChatEntry = null!; + [Signature("E8 ?? ?? ?? ?? 48 8D 4D A0 8B F8")] + private readonly delegate* unmanaged _getKeybind = null!; + + [Signature("E8 ?? ?? ?? ?? 48 3B F0 74 35")] + private readonly delegate* unmanaged _getFocus = null!; + + [Signature("44 8B 89 ?? ?? ?? ?? 4C 8B C1")] + private readonly delegate* unmanaged _getTellHistory = null!; + // Hooks private delegate byte ChatLogRefreshDelegate(IntPtr log, ushort eventId, AtkValue* value); @@ -33,6 +44,8 @@ internal sealed unsafe class Chat : IDisposable { private delegate void ReplyInSelectedChatModeDelegate(AgentInterface* agent); + private delegate byte SetChatLogTellTarget(IntPtr a1, Utf8String* name, Utf8String* a3, ushort world, ulong contentId, ushort a6, byte a7); + [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) @@ -51,6 +64,12 @@ internal sealed unsafe class Chat : IDisposable { )] private Hook? ReplyInSelectedChatModeHook { get; init; } + [Signature( + "E8 ?? ?? ?? ?? 4C 8B 7C 24 ?? EB 34", + DetourName = nameof(SetChatLogTellTargetDetour) + )] + private Hook? SetChatLogTellTargetHook { get; init; } + // Offsets #pragma warning disable 0649 @@ -65,7 +84,7 @@ internal sealed unsafe class Chat : IDisposable { // Events - internal delegate void ChatActivatedEventDelegate(string? input, InputChannel? channel, (string, string)? tellTarget); + internal delegate void ChatActivatedEventDelegate(string? input, ChannelSwitchInfo info); internal event ChatActivatedEventDelegate? Activated; @@ -79,14 +98,18 @@ internal sealed unsafe class Chat : IDisposable { this.ChatLogRefreshHook?.Enable(); this.ChangeChannelNameHook?.Enable(); this.ReplyInSelectedChatModeHook?.Enable(); + this.SetChatLogTellTargetHook?.Enable(); + this.Plugin.Framework.Update += this.InterceptKeybinds; this.Plugin.ClientState.Login += this.Login; this.Login(null, null); } public void Dispose() { this.Plugin.ClientState.Login -= this.Login; + this.Plugin.Framework.Update -= this.InterceptKeybinds; + this.SetChatLogTellTargetHook?.Dispose(); this.ReplyInSelectedChatModeHook?.Dispose(); this.ChangeChannelNameHook?.Dispose(); this.ChatLogRefreshHook?.Dispose(); @@ -94,6 +117,175 @@ internal sealed unsafe class Chat : IDisposable { this.Activated = null; } + private readonly Dictionary _keybinds = new(); + internal IReadOnlyDictionary Keybinds => this._keybinds; + + internal static readonly IReadOnlyDictionary KeybindsToIntercept = new Dictionary { + ["CMD_CHAT"] = new(null), + ["CMD_COMMAND"] = new(null, text: "/"), + ["CMD_REPLY"] = new(InputChannel.Tell, rotate: RotateMode.Forward), + ["CMD_REPLY_REV"] = new(InputChannel.Tell, rotate: RotateMode.Reverse), + ["CMD_SAY"] = new(InputChannel.Say), + ["CMD_YELL"] = new(InputChannel.Yell), + ["CMD_SHOUT"] = new(InputChannel.Shout), + ["CMD_PARTY"] = new(InputChannel.Party), + ["CMD_ALLIANCE"] = new(InputChannel.Alliance), + ["CMD_FREECOM"] = new(InputChannel.FreeCompany), + ["PVPTEAM_CHAT"] = new(InputChannel.PvpTeam), + ["CMD_CWLINKSHELL"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Forward), + ["CMD_CWLINKSHELL_REV"] = new(InputChannel.CrossLinkshell1, rotate: RotateMode.Reverse), + ["CMD_CWLINKSHELL_1"] = new(InputChannel.CrossLinkshell1), + ["CMD_CWLINKSHELL_2"] = new(InputChannel.CrossLinkshell2), + ["CMD_CWLINKSHELL_3"] = new(InputChannel.CrossLinkshell3), + ["CMD_CWLINKSHELL_4"] = new(InputChannel.CrossLinkshell4), + ["CMD_CWLINKSHELL_5"] = new(InputChannel.CrossLinkshell5), + ["CMD_CWLINKSHELL_6"] = new(InputChannel.CrossLinkshell6), + ["CMD_CWLINKSHELL_7"] = new(InputChannel.CrossLinkshell7), + ["CMD_CWLINKSHELL_8"] = new(InputChannel.CrossLinkshell8), + ["CMD_LINKSHELL"] = new(InputChannel.Linkshell1, rotate: RotateMode.Forward), + ["CMD_LINKSHELL_REV"] = new(InputChannel.Linkshell1, rotate: RotateMode.Reverse), + ["CMD_LINKSHELL_1"] = new(InputChannel.Linkshell1), + ["CMD_LINKSHELL_2"] = new(InputChannel.Linkshell2), + ["CMD_LINKSHELL_3"] = new(InputChannel.Linkshell3), + ["CMD_LINKSHELL_4"] = new(InputChannel.Linkshell4), + ["CMD_LINKSHELL_5"] = new(InputChannel.Linkshell5), + ["CMD_LINKSHELL_6"] = new(InputChannel.Linkshell6), + ["CMD_LINKSHELL_7"] = new(InputChannel.Linkshell7), + ["CMD_LINKSHELL_8"] = new(InputChannel.Linkshell8), + ["CMD_BEGINNER"] = new(InputChannel.NoviceNetwork), + ["CMD_REPLY_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Forward), + ["CMD_REPLY_REV_ALWAYS"] = new(InputChannel.Tell, true, RotateMode.Reverse), + ["CMD_SAY_ALWAYS"] = new(InputChannel.Say, true), + ["CMD_YELL_ALWAYS"] = new(InputChannel.Yell, true), + ["CMD_PARTY_ALWAYS"] = new(InputChannel.Party, true), + ["CMD_ALLIANCE_ALWAYS"] = new(InputChannel.Alliance, true), + ["CMD_FREECOM_ALWAYS"] = new(InputChannel.FreeCompany, true), + ["PVPTEAM_CHAT_ALWAYS"] = new(InputChannel.PvpTeam, true), + ["CMD_CWLINKSHELL_ALWAYS"] = new(InputChannel.CrossLinkshell1, true, RotateMode.Forward), + ["CMD_CWLINKSHELL_ALWAYS_REV"] = new(InputChannel.CrossLinkshell1, true, RotateMode.Reverse), + ["CMD_CWLINKSHELL_1_ALWAYS"] = new(InputChannel.CrossLinkshell1, true), + ["CMD_CWLINKSHELL_2_ALWAYS"] = new(InputChannel.CrossLinkshell2, true), + ["CMD_CWLINKSHELL_3_ALWAYS"] = new(InputChannel.CrossLinkshell3, true), + ["CMD_CWLINKSHELL_4_ALWAYS"] = new(InputChannel.CrossLinkshell4, true), + ["CMD_CWLINKSHELL_5_ALWAYS"] = new(InputChannel.CrossLinkshell5, true), + ["CMD_CWLINKSHELL_6_ALWAYS"] = new(InputChannel.CrossLinkshell6, true), + ["CMD_CWLINKSHELL_7_ALWAYS"] = new(InputChannel.CrossLinkshell7, true), + ["CMD_CWLINKSHELL_8_ALWAYS"] = new(InputChannel.CrossLinkshell8, true), + ["CMD_LINKSHELL_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Forward), + ["CMD_LINKSHELL_REV_ALWAYS"] = new(InputChannel.Linkshell1, true, RotateMode.Reverse), + ["CMD_LINKSHELL_1_ALWAYS"] = new(InputChannel.Linkshell1, true), + ["CMD_LINKSHELL_2_ALWAYS"] = new(InputChannel.Linkshell2, true), + ["CMD_LINKSHELL_3_ALWAYS"] = new(InputChannel.Linkshell3, true), + ["CMD_LINKSHELL_4_ALWAYS"] = new(InputChannel.Linkshell4, true), + ["CMD_LINKSHELL_5_ALWAYS"] = new(InputChannel.Linkshell5, true), + ["CMD_LINKSHELL_6_ALWAYS"] = new(InputChannel.Linkshell6, true), + ["CMD_LINKSHELL_7_ALWAYS"] = new(InputChannel.Linkshell7, true), + ["CMD_LINKSHELL_8_ALWAYS"] = new(InputChannel.Linkshell8, true), + ["CMD_BEGINNER_ALWAYS"] = new(InputChannel.NoviceNetwork, true), + }; + + private bool _inputFocused; + private int _graceFrames; + + + private void CheckFocus() { + void Decrement() { + if (this._graceFrames > 0) { + this._graceFrames -= 1; + } else { + this._inputFocused = false; + } + } + + var focus = this._getFocus(AtkStage.GetSingleton()); + if (focus == IntPtr.Zero) { + Decrement(); + return; + } + + var node = (AtkResNode*) focus; + var parent = node->ParentNode; + if (parent == null || (uint) parent->Type is not (1007 or 1011)) { + Decrement(); + return; + } + + this._inputFocused = true; + this._graceFrames = 60; + } + + private void UpdateKeybinds() { + foreach (var name in KeybindsToIntercept.Keys) { + var keybind = this.GetKeybind(name); + if (keybind is null) { + continue; + } + + this._keybinds[name] = keybind; + } + } + + private void InterceptKeybinds(Dalamud.Game.Framework framework) { + this.CheckFocus(); + this.UpdateKeybinds(); + + if (this._inputFocused) { + return; + } + + var modifierState = (ModifierFlag) 0; + foreach (var modifier in Enum.GetValues()) { + var modifierKey = GetKeyForModifier(modifier); + if (modifierKey != VirtualKey.NO_KEY && this.Plugin.KeyState[modifierKey]) { + modifierState |= modifier; + } + } + + var turnedOff = new Dictionary(); + foreach (var toIntercept in KeybindsToIntercept.Keys) { + if (!this.Keybinds.TryGetValue(toIntercept, out var keybind)) { + continue; + } + + void Intercept(VirtualKey key, ModifierFlag modifier) { + if (!this.Plugin.KeyState.IsVirtualKeyValid(key)) { + return; + } + + if (!modifierState.HasFlag(modifier)) { + return; + } + + if (!this.Plugin.KeyState[key]) { + return; + } + + var bits = NumUtil.NumberOfSetBits((uint) modifier); + if (!turnedOff.TryGetValue(key, out var previousBits) || previousBits.Item1 < bits) { + turnedOff[key] = (bits, toIntercept); + } + } + + Intercept(keybind.Key1, keybind.Modifier1); + Intercept(keybind.Key2, keybind.Modifier2); + } + + foreach (var (key, (_, keybind)) in turnedOff) { + PluginLog.Log($"intercepting {keybind}"); + this.Plugin.KeyState[key] = false; + + if (!KeybindsToIntercept.TryGetValue(keybind, out var info)) { + continue; + } + + try { + this.Activated?.Invoke(null, info); + } catch (Exception ex) { + PluginLog.LogError(ex, "Error in chat Activated event"); + } + } + } + private void Login(object? sender, EventArgs? e) { if (this.ChangeChannelNameHook == null) { return; @@ -107,6 +299,31 @@ internal sealed unsafe class Chat : IDisposable { this.ChangeChannelNameDetour((IntPtr) agent); } + private byte ChatLogRefreshDetour(IntPtr log, ushort eventId, AtkValue* value) { + if (eventId != 0x31 || value == null || value->UInt is not (0x05 or 0x0C)) { + 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, new ChannelSwitchInfo(null)); + } catch (Exception ex) { + PluginLog.LogError(ex, "Error in chat Activated event"); + } + + // prevent the game from focusing the chat log + return 1; + } + private IntPtr ChangeChannelNameDetour(IntPtr agent) { // Last ShB patch // +0x40 = chat channel (byte or uint?) @@ -158,56 +375,6 @@ internal sealed unsafe class Chat : IDisposable { return ret; } - private byte ChatLogRefreshDetour(IntPtr log, ushort eventId, AtkValue* value) { - if (eventId != 0x31 || value == null || value->UInt is not (0x05 or 0x0C)) { - return this.ChatLogRefreshHook!.Original(log, eventId, value); - } - - InputChannel? channel = null; - (string, string)? tellTarget = null; - // if (this._shellChannelOffset != null) { - // var shell = (IntPtr) Framework.Instance()->GetUiModule()->GetRaptureShellModule(); - // - // channel = (InputChannel) (*(uint*) (shell + this._shellChannelOffset.Value)); - // if ((int) channel is 17 or 18) { - // channel = InputChannel.Tell; - // - // var targetPtr = *(byte**) (shell + 0xFD8); - // var targetWorldPtr = *(byte**) (shell + 0x1040); - // - // var target = MemoryHelper.ReadStringNullTerminated((IntPtr) targetPtr); - // var targetWorld = MemoryHelper.ReadStringNullTerminated((IntPtr) targetWorldPtr); - // tellTarget = (target, targetWorld); - // } - // } - - 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, channel, tellTarget); - } catch (Exception ex) { - PluginLog.LogError(ex, "Error in ChatActivated event"); - } - - // var ret = this.ChatLogRefreshHook!.Original(log, eventId, value); - // if (this._clearFocus != null) { - // this._clearFocus(AtkStage.GetSingleton()); - // } - - // return ret; - - // return this.ChatLogRefreshHook!.Original(log, eventId, value); - return 1; - } - private void ReplyInSelectedChatModeDetour(AgentInterface* agent) { if (this._replyChannelOffset == null) { goto Original; @@ -224,6 +391,14 @@ internal sealed unsafe class Chat : IDisposable { this.ReplyInSelectedChatModeHook!.Original(agent); } + private byte SetChatLogTellTargetDetour(IntPtr a1, Utf8String* name, Utf8String* a3, ushort world, ulong contentId, ushort reason, byte a7) { + if (name != null) { + PluginLog.Log($"{name->ToString()}@{world} ({contentId}) {(TellReason) reason}"); + } + + return this.SetChatLogTellTargetHook!.Original(a1, name, a3, world, contentId, reason, a7); + } + internal ulong? GetContentIdForEntry(uint index) { if (this._getContentIdForChatEntry == null) { return null; @@ -254,4 +429,62 @@ internal sealed unsafe class Chat : IDisposable { this._changeChatChannel(RaptureShellModule.Instance, (int) channel, idx, &target, 1); } } + + private static VirtualKey GetKeyForModifier(ModifierFlag modifierFlag) => modifierFlag switch { + ModifierFlag.Shift => VirtualKey.SHIFT, + ModifierFlag.Ctrl => VirtualKey.CONTROL, + ModifierFlag.Alt => VirtualKey.MENU, + _ => VirtualKey.NO_KEY, + }; + + private Keybind? GetKeybind(string id) { + var agent = (IntPtr) Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.Configkey); + if (agent == IntPtr.Zero) { + return null; + } + + var a1 = *(void**) (agent + 0x78); + if (a1 == null) { + return null; + } + + var outData = stackalloc byte[32]; + var idString = Utf8String.FromString(id); + this._getKeybind((IntPtr) a1, idString, (IntPtr) outData); + + var key1 = (VirtualKey) outData[0]; + if (key1 is VirtualKey.F23) { + key1 = VirtualKey.OEM_2; + } + + var key2 = (VirtualKey) outData[2]; + if (key2 is VirtualKey.F23) { + key2 = VirtualKey.OEM_2; + } + + return new Keybind { + Key1 = key1, + Modifier1 = (ModifierFlag) outData[1], + Key2 = key2, + Modifier2 = (ModifierFlag) outData[3], + }; + } + + internal TellHistoryInfo? GetTellHistoryInfo(int index) { + var acquaintanceModule = Framework.Instance()->GetUiModule()->GetAcquaintanceModule(); + if (acquaintanceModule == null) { + return null; + } + + var ptr = this._getTellHistory(acquaintanceModule, index); + if (ptr == IntPtr.Zero) { + return null; + } + + var name = MemoryHelper.ReadStringNullTerminated(*(IntPtr*) ptr); + var world = *(ushort*) (ptr + 0xD0); + var contentId = *(ulong*) (ptr + 0xD8); + + return new TellHistoryInfo(name, world, contentId); + } } diff --git a/ChatTwo/GameFunctions/Types/ChannelSwitchInfo.cs b/ChatTwo/GameFunctions/Types/ChannelSwitchInfo.cs new file mode 100755 index 0000000..0711f14 --- /dev/null +++ b/ChatTwo/GameFunctions/Types/ChannelSwitchInfo.cs @@ -0,0 +1,17 @@ +using ChatTwo.Code; + +namespace ChatTwo.GameFunctions.Types; + +internal class ChannelSwitchInfo { + internal InputChannel? Channel { get; } + internal bool Permanent { get; } + internal RotateMode Rotate { get; } + internal string? Text { get; } + + internal ChannelSwitchInfo(InputChannel? channel, bool permanent = false, RotateMode rotate = RotateMode.None, string? text = null) { + this.Channel = channel; + this.Permanent = permanent; + this.Rotate = rotate; + this.Text = text; + } +} diff --git a/ChatTwo/GameFunctions/Types/Keybind.cs b/ChatTwo/GameFunctions/Types/Keybind.cs new file mode 100755 index 0000000..4274161 --- /dev/null +++ b/ChatTwo/GameFunctions/Types/Keybind.cs @@ -0,0 +1,11 @@ +using Dalamud.Game.ClientState.Keys; + +namespace ChatTwo.GameFunctions.Types; + +internal class Keybind { + internal VirtualKey Key1 { get; init; } + internal ModifierFlag Modifier1 { get; init; } + + internal VirtualKey Key2 { get; init; } + internal ModifierFlag Modifier2 { get; init; } +} diff --git a/ChatTwo/GameFunctions/Types/ModifierFlag.cs b/ChatTwo/GameFunctions/Types/ModifierFlag.cs new file mode 100755 index 0000000..9abc5eb --- /dev/null +++ b/ChatTwo/GameFunctions/Types/ModifierFlag.cs @@ -0,0 +1,8 @@ +namespace ChatTwo.GameFunctions.Types; + +[Flags] +internal enum ModifierFlag { + Shift = 1 << 0, + Ctrl = 1 << 1, + Alt = 1 << 2, +} diff --git a/ChatTwo/GameFunctions/Types/RotateMode.cs b/ChatTwo/GameFunctions/Types/RotateMode.cs new file mode 100755 index 0000000..95a94af --- /dev/null +++ b/ChatTwo/GameFunctions/Types/RotateMode.cs @@ -0,0 +1,7 @@ +namespace ChatTwo.GameFunctions.Types; + +internal enum RotateMode { + None, + Forward, + Reverse, +} diff --git a/ChatTwo/GameFunctions/Types/TellHistoryInfo.cs b/ChatTwo/GameFunctions/Types/TellHistoryInfo.cs new file mode 100755 index 0000000..d07d86d --- /dev/null +++ b/ChatTwo/GameFunctions/Types/TellHistoryInfo.cs @@ -0,0 +1,13 @@ +namespace ChatTwo.GameFunctions.Types; + +internal sealed class TellHistoryInfo { + internal string Name { get; } + internal uint World { get; } + internal ulong ContentId { get; } + + internal TellHistoryInfo(string name, uint world, ulong contentId) { + this.Name = name; + this.World = world; + this.ContentId = contentId; + } +} diff --git a/ChatTwo/GameFunctions/Types/TellReason.cs b/ChatTwo/GameFunctions/Types/TellReason.cs new file mode 100755 index 0000000..d8f9f83 --- /dev/null +++ b/ChatTwo/GameFunctions/Types/TellReason.cs @@ -0,0 +1,7 @@ +namespace ChatTwo.GameFunctions.Types; + +internal enum TellReason { + Direct = 0, + PartyFinder = 1, + Friend = 3, +} diff --git a/ChatTwo/PayloadHandler.cs b/ChatTwo/PayloadHandler.cs index d5ea0ef..d426c17 100755 --- a/ChatTwo/PayloadHandler.cs +++ b/ChatTwo/PayloadHandler.cs @@ -154,6 +154,10 @@ internal sealed class PayloadHandler { } private void HoverItem(ItemPayload item) { + if (item.Item == null) { + return; + } + if (this.Ui.Plugin.TextureCache.GetItem(item.Item, item.IsHQ) is { } icon) { InlineIcon(icon); } @@ -232,6 +236,10 @@ internal sealed class PayloadHandler { } private void DrawItemPopup(ItemPayload item) { + if (item.Item == null) { + return; + } + if (this.Ui.Plugin.TextureCache.GetItem(item.Item, item.IsHQ) is { } icon) { InlineIcon(icon); } diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index 7a4bcb0..f2fbeb4 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -1,6 +1,7 @@ using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Party; using Dalamud.Game.Command; @@ -35,6 +36,9 @@ public sealed class Plugin : IDalamudPlugin { [PluginService] internal GameGui GameGui { get; init; } + + [PluginService] + internal KeyState KeyState { get; init; } [PluginService] internal ObjectTable ObjectTable { get; init; } diff --git a/ChatTwo/Ui/ChatLog.cs b/ChatTwo/Ui/ChatLog.cs index ffc244c..e3f5383 100755 --- a/ChatTwo/Ui/ChatLog.cs +++ b/ChatTwo/Ui/ChatLog.cs @@ -1,11 +1,12 @@ using System.Numerics; using ChatTwo.Code; +using ChatTwo.GameFunctions.Types; using ChatTwo.Util; +using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Command; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; -using Dalamud.Memory; -using Dalamud.Utility; +using Dalamud.Logging; using ImGuiNET; using ImGuiScene; using Lumina.Excel.GeneratedSheets; @@ -24,7 +25,8 @@ internal sealed class ChatLog : IUiComponent { private int _inputBacklogIdx = -1; private int _lastTab; private InputChannel? _tempChannel; - private (string, string)? _tellTarget; + private (string, string, ulong)? _tellTarget; + private int _tellIdx; private PayloadHandler PayloadHandler { get; } private Dictionary TextCommandChannels { get; } = new(); @@ -50,17 +52,44 @@ internal sealed class ChatLog : IUiComponent { this.Ui.Plugin.CommandManager.RemoveHandler("/clearlog2"); } - private void Activated(string? input, InputChannel? channel, (string, string)? tellTarget) { + private void Activated(string? input, ChannelSwitchInfo info) { this.Activate = true; if (input != null && !this.Chat.Contains(input)) { this.Chat += input; } - if (channel != null && this.Ui.Plugin.Functions.Chat.Channel.channel != channel) { - this._tempChannel = channel; + if (info.Channel != null) { + var prevTemp = this._tempChannel; + + if (info.Permanent) { + this.Ui.Plugin.Functions.Chat.SetChannel(info.Channel.Value); + } else { + this._tempChannel = info.Channel.Value; + } + + if (info.Channel is InputChannel.Tell && info.Rotate != RotateMode.None) { + this._tellIdx = prevTemp != InputChannel.Tell + ? 0 + : info.Rotate == RotateMode.Reverse + ? -1 + : 1; + + var tellInfo = this.Ui.Plugin.Functions.Chat.GetTellHistoryInfo(this._tellIdx); + if (tellInfo != null) { + var world = this.Ui.Plugin.DataManager.GetExcelSheet()?.GetRow(tellInfo.World); + if (world != null) { + this._tellTarget = (tellInfo.Name, world.Name, tellInfo.ContentId); + } + } + } else { + this._tellIdx = 0; + this._tellTarget = null; + } } - this._tellTarget = tellTarget; + if (info.Text != null && this.Chat.Length == 0) { + this.Chat = info.Text; + } } private void ClearLog(string command, string arguments) { @@ -140,6 +169,55 @@ internal sealed class ChatLog : IUiComponent { private unsafe ImGuiViewport* _lastViewport; + private void HandleKeybinds() { + var modifierState = (ModifierFlag) 0; + if (ImGui.GetIO().KeyAlt) { + modifierState |= ModifierFlag.Alt; + } + + if (ImGui.GetIO().KeyCtrl) { + modifierState |= ModifierFlag.Ctrl; + } + + if (ImGui.GetIO().KeyShift) { + modifierState |= ModifierFlag.Shift; + } + + var turnedOff = new Dictionary(); + foreach (var (toIntercept, keybind) in this.Ui.Plugin.Functions.Chat.Keybinds) { + if (toIntercept is "CMD_CHAT" or "CMD_COMMAND") { + continue; + } + + void Intercept(VirtualKey key, ModifierFlag modifier) { + if (!ImGui.IsKeyPressed((int) key) || !modifierState.HasFlag(modifier)) { + return; + } + + var bits = NumUtil.NumberOfSetBits((uint) modifier); + if (!turnedOff.TryGetValue(key, out var previousBits) || previousBits.Item1 < bits) { + turnedOff[key] = (bits, toIntercept); + } + } + + Intercept(keybind.Key1, keybind.Modifier1); + Intercept(keybind.Key2, keybind.Modifier2); + } + + foreach (var (_, (_, keybind)) in turnedOff) { + PluginLog.Log($"intercepting {keybind}"); + if (!GameFunctions.Chat.KeybindsToIntercept.TryGetValue(keybind, out var info)) { + continue; + } + + try { + this.Activated(null, info); + } catch (Exception ex) { + PluginLog.LogError(ex, "Error in chat Activated event"); + } + } + } + public unsafe void Draw() { var flags = ImGuiWindowFlags.None; if (!this.Ui.Plugin.Config.CanMove) { @@ -271,8 +349,13 @@ internal sealed class ChatLog : IUiComponent { this.Chat = string.Empty; } + if (ImGui.IsItemActive()) { + this.HandleKeybinds(); + } + if (!this.Activate && !ImGui.IsItemActive()) { this._tempChannel = null; + this._tellIdx = 0; } if (inputColour != null) { diff --git a/ChatTwo/Util/NumUtil.cs b/ChatTwo/Util/NumUtil.cs new file mode 100755 index 0000000..d28bbdc --- /dev/null +++ b/ChatTwo/Util/NumUtil.cs @@ -0,0 +1,9 @@ +namespace ChatTwo.Util; + +internal static class NumUtil { + internal static uint NumberOfSetBits(uint i) { + i -= (i >> 1) & 0x55555555; + i = (i & 0x33333333) + ((i >> 2) & 0x33333333); + return (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24; + } +}