From fb167a81610c8c1ca3515151537b7bea3b163ea9 Mon Sep 17 00:00:00 2001 From: Dean Sheather Date: Mon, 15 Jul 2024 19:16:30 +1000 Subject: [PATCH] chore: refactor keybinds to be in new KeybindManager --- ChatTwo/GameFunctions/Chat.cs | 238 +----------- ChatTwo/GameFunctions/GameFunctions.cs | 3 + ChatTwo/GameFunctions/KeybindManager.cs | 482 ++++++++++++++++++++++++ ChatTwo/Ui/ChatLogWindow.cs | 86 +---- 4 files changed, 494 insertions(+), 315 deletions(-) create mode 100644 ChatTwo/GameFunctions/KeybindManager.cs diff --git a/ChatTwo/GameFunctions/Chat.cs b/ChatTwo/GameFunctions/Chat.cs index d36b307..a6da4fa 100755 --- a/ChatTwo/GameFunctions/Chat.cs +++ b/ChatTwo/GameFunctions/Chat.cs @@ -1,15 +1,11 @@ -using System.Numerics; -using System.Text; +using System.Text; using ChatTwo.Code; using ChatTwo.GameFunctions.Types; using ChatTwo.Resources; using ChatTwo.Util; -using Dalamud.Game.ClientState.Keys; -using Dalamud.Game.Config; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; using Dalamud.Memory; -using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using FFXIVClientStructs.FFXIV.Application.Network; using FFXIVClientStructs.FFXIV.Client.System.Framework; @@ -29,10 +25,6 @@ namespace ChatTwo.GameFunctions; internal sealed unsafe class Chat : IDisposable { // Functions - // Replace with - [Signature("E8 ?? ?? ?? ?? 48 8D 4D A0 8B F8")] - private readonly delegate* unmanaged GetKeybindNative = null!; - [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D B9 ?? ?? ?? ?? 33 C0")] private readonly delegate* unmanaged PrintTellNative = null!; @@ -54,11 +46,6 @@ internal sealed unsafe class Chat : IDisposable [Signature("48 8D 35 ?? ?? ?? ?? 8B 05", ScanType = ScanType.StaticAddress)] private readonly char* CurrentCharacter = null!; - // Events - - internal event ChatActivatedEventDelegate? Activated; - internal delegate void ChatActivatedEventDelegate(ChatActivatedArgs args); - private Plugin Plugin { get; } /// @@ -70,9 +57,6 @@ internal sealed unsafe class Chat : IDisposable internal bool UsesTellTempChannel { get; set; } internal InputChannel? PreviousChannel { get; private set; } - private bool DirectChat; - private long LastRefresh; - internal Chat(Plugin plugin) { Plugin = plugin; @@ -92,7 +76,6 @@ internal sealed unsafe class Chat : IDisposable // EurekaContextMenuTellHook = Plugin.GameInteropProvider.HookFromAddress(RaptureShellModule.MemberFunctionPointers.SetContextTellTargetInForay, SetContextTellTargetInForay); // EurekaContextMenuTellHook.Enable(); - Plugin.Framework.Update += InterceptKeybinds; Plugin.ClientState.Login += Login; Login(); } @@ -100,15 +83,12 @@ internal sealed unsafe class Chat : IDisposable public void Dispose() { Plugin.ClientState.Login -= Login; - Plugin.Framework.Update -= InterceptKeybinds; SetChatLogTellTargetHook?.Dispose(); ReplyInSelectedChatModeHook?.Dispose(); ChangeChannelNameHook?.Dispose(); ChatLogRefreshHook?.Dispose(); EurekaContextMenuTellHook?.Dispose(); - - Activated = null; } internal string? GetLinkshellName(uint idx) @@ -164,182 +144,6 @@ internal sealed unsafe class Chat : IDisposable return 0xFF | (rgb << 8); } - private readonly Dictionary _keybinds = new(); - internal IReadOnlyDictionary Keybinds => _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 void UpdateKeybinds() - { - foreach (var name in KeybindsToIntercept.Keys) - { - var keybind = GetKeybind(name); - if (keybind is null) - continue; - - _keybinds[name] = keybind; - } - } - - private void InterceptKeybinds(IFramework framework1) - { - // Refresh current keybinds every 5s - if (LastRefresh + 5 * 1000 < Environment.TickCount64) - { - UpdateKeybinds(); - DirectChat = Plugin.GameConfig.TryGet(UiControlOption.DirectChat, out bool option) && option; - LastRefresh = Environment.TickCount64; - } - - // Vanilla text input has focus - if (RaptureAtkModule.Instance()->AtkModule.IsTextInputActive()) - return; - - var modifierState = (ModifierFlag) 0; - foreach (var modifier in Enum.GetValues()) - { - var modifierKey = GetKeyForModifier(modifier); - if (modifierKey != VirtualKey.NO_KEY && Plugin.KeyState[modifierKey]) - modifierState |= modifier; - } - - bool KeyPressed(VirtualKey key, ModifierFlag modifier) - { - if (!Plugin.KeyState.IsVirtualKeyValid(key)) - return false; - - var modifierPressed = Plugin.Config.KeybindMode switch - { - KeybindMode.Strict => modifier == modifierState, - KeybindMode.Flexible => modifierState.HasFlag(modifier), - _ => false, - }; - - return modifierPressed && Plugin.KeyState[key]; - } - - // Test for custom keybinds for changing chat tabs before checking - // vanilla keybinds. - if (Plugin.Config.ChatTabBackward != null && KeyPressed(Plugin.Config.ChatTabBackward.Key, Plugin.Config.ChatTabBackward.Modifier)) - { - Plugin.KeyState[Plugin.Config.ChatTabBackward.Key] = false; - Plugin.ChatLogWindow.ChangeTabDelta(-1); - return; - } - if (Plugin.Config.ChatTabForward != null && KeyPressed(Plugin.Config.ChatTabForward.Key, Plugin.Config.ChatTabForward.Modifier)) - { - Plugin.KeyState[Plugin.Config.ChatTabForward.Key] = false; - Plugin.ChatLogWindow.ChangeTabDelta(1); - return; - } - - var turnedOff = new Dictionary(); - foreach (var toIntercept in KeybindsToIntercept.Keys) - { - if (!Keybinds.TryGetValue(toIntercept, out var keybind)) - continue; - - if (toIntercept is "CMD_CHAT" or "CMD_COMMAND") - { - // Direct chat option is selected - if (DirectChat) - continue; - } - - void Intercept(VirtualKey key, ModifierFlag modifier) - { - if (!KeyPressed(key, modifier)) - return; - - var bits = BitOperations.PopCount((uint) modifier); - if (!turnedOff.TryGetValue(key, out var previousBits) || previousBits.Item1 < bits) - turnedOff[key] = ((uint) bits, toIntercept); - } - - Intercept(keybind.Key1, keybind.Modifier1); - Intercept(keybind.Key2, keybind.Modifier2); - } - - foreach (var (key, (_, keybind)) in turnedOff) - { - Plugin.KeyState[key] = false; - if (!KeybindsToIntercept.TryGetValue(keybind, out var info)) - continue; - - try - { - Activated?.Invoke(new ChatActivatedArgs(info) { TellReason = TellReason.Reply, }); - } - catch (Exception ex) - { - Plugin.Log.Error(ex, "Error in chat Activated event"); - } - } - } - private void Login() { var agent = AgentChatLog.Instance(); @@ -357,7 +161,7 @@ internal sealed unsafe class Chat : IDisposable if (eventId != 0x31 || value == null || value->UInt is not (0x05 or 0x0C)) return ChatLogRefreshHook!.Original(log, eventId, value); - if (DirectChat && CurrentCharacter != null) + if (Plugin.Functions.KeybindManager.DirectChat && CurrentCharacter != null) { // FIXME: this whole system sucks // FIXME v2: I hate everything about this, but it works @@ -376,7 +180,7 @@ internal sealed unsafe class Chat : IDisposable try { - Activated?.Invoke(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { Input = input, }); + Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { Input = input, }); } catch (Exception ex) { @@ -397,7 +201,7 @@ internal sealed unsafe class Chat : IDisposable try { - Activated?.Invoke(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { AddIfNotPresent = addIfNotPresent, }); + Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { AddIfNotPresent = addIfNotPresent, }); } catch (Exception ex) { @@ -463,7 +267,7 @@ internal sealed unsafe class Chat : IDisposable try { var target = new TellTarget(playerName->ToString(), worldId, contentId, (TellReason) reason); - Activated?.Invoke(new ChatActivatedArgs(new ChannelSwitchInfo(InputChannel.Tell)) + Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(InputChannel.Tell)) { TellReason = (TellReason) reason, TellTarget = target, @@ -547,38 +351,6 @@ internal sealed unsafe class Chat : IDisposable utfWorld->Dtor(true); } - 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 outData = stackalloc byte[11]; - var idString = Utf8String.FromString(id); - GetKeybindNative(UIInputData.Instance(), idString, (nint) outData); - idString->Dtor(true); - - 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 acquaintance = AcquaintanceModule.Instance()->GetTellHistory(index); diff --git a/ChatTwo/GameFunctions/GameFunctions.cs b/ChatTwo/GameFunctions/GameFunctions.cs index 8de4a52..59200fe 100755 --- a/ChatTwo/GameFunctions/GameFunctions.cs +++ b/ChatTwo/GameFunctions/GameFunctions.cs @@ -25,11 +25,13 @@ internal unsafe class GameFunctions : IDisposable #endregion private Plugin Plugin { get; } + internal KeybindManager KeybindManager { get; } internal Chat Chat { get; } internal GameFunctions(Plugin plugin) { Plugin = plugin; + KeybindManager = new KeybindManager(plugin); Chat = new Chat(Plugin); Plugin.GameInteropProvider.InitializeFromAttributes(this); @@ -40,6 +42,7 @@ internal unsafe class GameFunctions : IDisposable public void Dispose() { Chat.Dispose(); + KeybindManager.Dispose(); ResolveTextCommandPlaceholderHook?.Dispose(); diff --git a/ChatTwo/GameFunctions/KeybindManager.cs b/ChatTwo/GameFunctions/KeybindManager.cs new file mode 100644 index 0000000..d83beea --- /dev/null +++ b/ChatTwo/GameFunctions/KeybindManager.cs @@ -0,0 +1,482 @@ +using System.Numerics; +using ChatTwo.Code; +using ChatTwo.GameFunctions.Types; +using ChatTwo.Util; +using Dalamud.Game.ClientState.Keys; +using Dalamud.Game.Config; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI; +using ImGuiNET; + +namespace ChatTwo.GameFunctions; + +internal enum KeyboardSource { + Game, + ImGui +} + +internal unsafe class KeybindManager : IDisposable { + // Functions + // Replace with + [Signature("E8 ?? ?? ?? ?? 48 8D 4D A0 8B F8")] + private readonly delegate* unmanaged GetKeybindNative = null!; + + private Plugin Plugin { get; } + + internal bool DirectChat; + private long LastRefresh; + + private readonly Dictionary Keybinds = new(); + private 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) + }; + + // List of keys that can be used as a part of keybinds while the chat is + // focused WITHOUT modifiers. All other keys can only be used if their + // configured keybind contains modifiers. This allows for using e.g. F11 to + // change chat channel while typing. + private static readonly IReadOnlyCollection ModifierlessChatKeys = new[] + { + // VirtualKey.NO_KEY, + VirtualKey.LBUTTON, + VirtualKey.RBUTTON, + VirtualKey.CANCEL, + VirtualKey.MBUTTON, + VirtualKey.XBUTTON1, + VirtualKey.XBUTTON2, + VirtualKey.BACK, + VirtualKey.TAB, + VirtualKey.CLEAR, + // VirtualKey.RETURN, // handled by imgui + // VirtualKey.SHIFT, + // VirtualKey.CONTROL, + // VirtualKey.MENU, + VirtualKey.PAUSE, + // VirtualKey.CAPITAL, + // VirtualKey.KANA, + // VirtualKey.HANGUL, + // VirtualKey.JUNJA, + // VirtualKey.FINAL, + // VirtualKey.HANJA, + // VirtualKey.KANJI, + VirtualKey.ESCAPE, + // VirtualKey.CONVERT, + // VirtualKey.NONCONVERT, + // VirtualKey.ACCEPT, + // VirtualKey.MODECHANGE, + // VirtualKey.SPACE, + VirtualKey.PRIOR, + VirtualKey.NEXT, + VirtualKey.END, + VirtualKey.HOME, + // VirtualKey.LEFT, // handled by imgui + VirtualKey.UP, + // VirtualKey.RIGHT, // handled by imgui + VirtualKey.DOWN, + VirtualKey.SELECT, + VirtualKey.PRINT, + VirtualKey.EXECUTE, + VirtualKey.SNAPSHOT, + VirtualKey.INSERT, + VirtualKey.DELETE, + VirtualKey.HELP, + // VirtualKey.KEY_0, + // VirtualKey.KEY_1, + // VirtualKey.KEY_2, + // VirtualKey.KEY_3, + // VirtualKey.KEY_4, + // VirtualKey.KEY_5, + // VirtualKey.KEY_6, + // VirtualKey.KEY_7, + // VirtualKey.KEY_8, + // VirtualKey.KEY_9, + // VirtualKey.A, + // VirtualKey.B, + // VirtualKey.C, + // VirtualKey.D, + // VirtualKey.E, + // VirtualKey.F, + // VirtualKey.G, + // VirtualKey.H, + // VirtualKey.I, + // VirtualKey.J, + // VirtualKey.K, + // VirtualKey.L, + // VirtualKey.M, + // VirtualKey.N, + // VirtualKey.O, + // VirtualKey.P, + // VirtualKey.Q, + // VirtualKey.R, + // VirtualKey.S, + // VirtualKey.T, + // VirtualKey.U, + // VirtualKey.V, + // VirtualKey.W, + // VirtualKey.X, + // VirtualKey.Y, + // VirtualKey.Z, + // VirtualKey.LWIN, + // VirtualKey.RWIN, + VirtualKey.APPS, + VirtualKey.SLEEP, + // VirtualKey.NUMPAD0, + // VirtualKey.NUMPAD1, + // VirtualKey.NUMPAD2, + // VirtualKey.NUMPAD3, + // VirtualKey.NUMPAD4, + // VirtualKey.NUMPAD5, + // VirtualKey.NUMPAD6, + // VirtualKey.NUMPAD7, + // VirtualKey.NUMPAD8, + // VirtualKey.NUMPAD9, + // VirtualKey.MULTIPLY, + // VirtualKey.ADD, + // VirtualKey.SEPARATOR, + // VirtualKey.SUBTRACT, + // VirtualKey.DECIMAL, + // VirtualKey.DIVIDE, + VirtualKey.F1, + VirtualKey.F2, + VirtualKey.F3, + VirtualKey.F4, + VirtualKey.F5, + VirtualKey.F6, + VirtualKey.F7, + VirtualKey.F8, + VirtualKey.F9, + VirtualKey.F10, + VirtualKey.F11, + VirtualKey.F12, + VirtualKey.F13, + VirtualKey.F14, + VirtualKey.F15, + VirtualKey.F16, + VirtualKey.F17, + VirtualKey.F18, + VirtualKey.F19, + VirtualKey.F20, + VirtualKey.F21, + VirtualKey.F22, + VirtualKey.F23, + VirtualKey.F24, + VirtualKey.NUMLOCK, + VirtualKey.SCROLL, + // VirtualKey.OEM_FJ_JISHO, + // VirtualKey.OEM_NEC_EQUAL, + // VirtualKey.OEM_FJ_MASSHOU, + // VirtualKey.OEM_FJ_TOUROKU, + // VirtualKey.OEM_FJ_LOYA, + // VirtualKey.OEM_FJ_ROYA, + // VirtualKey.LSHIFT, + // VirtualKey.RSHIFT, + // VirtualKey.LCONTROL, + // VirtualKey.RCONTROL, + // VirtualKey.LMENU, + // VirtualKey.RMENU, + VirtualKey.BROWSER_BACK, + VirtualKey.BROWSER_FORWARD, + VirtualKey.BROWSER_REFRESH, + VirtualKey.BROWSER_STOP, + VirtualKey.BROWSER_SEARCH, + VirtualKey.BROWSER_FAVORITES, + VirtualKey.BROWSER_HOME, + VirtualKey.VOLUME_MUTE, + VirtualKey.VOLUME_DOWN, + VirtualKey.VOLUME_UP, + VirtualKey.MEDIA_NEXT_TRACK, + VirtualKey.MEDIA_PREV_TRACK, + VirtualKey.MEDIA_STOP, + VirtualKey.MEDIA_PLAY_PAUSE, + VirtualKey.LAUNCH_MAIL, + VirtualKey.LAUNCH_MEDIA_SELECT, + VirtualKey.LAUNCH_APP1, + VirtualKey.LAUNCH_APP2, + // VirtualKey.OEM_1, + // VirtualKey.OEM_PLUS, + // VirtualKey.OEM_COMMA, + // VirtualKey.OEM_MINUS, + // VirtualKey.OEM_PERIOD, + // VirtualKey.OEM_2, + // VirtualKey.OEM_3, + // VirtualKey.OEM_4, // [{ + // VirtualKey.OEM_5, // \" + // VirtualKey.OEM_6, // ]} + // VirtualKey.OEM_7, // '" + // VirtualKey.OEM_8, + // VirtualKey.OEM_AX, + // VirtualKey.OEM_102, + // VirtualKey.ICO_HELP, + // VirtualKey.ICO_00, + // VirtualKey.PROCESSKEY, + // VirtualKey.ICO_CLEAR, + // VirtualKey.PACKET, + // VirtualKey.OEM_RESET, + // VirtualKey.OEM_JUMP, + // VirtualKey.OEM_PA1, + // VirtualKey.OEM_PA2, + // VirtualKey.OEM_PA3, + // VirtualKey.OEM_WSCTRL, + // VirtualKey.OEM_CUSEL, + // VirtualKey.OEM_ATTN, + // VirtualKey.OEM_FINISH, + // VirtualKey.OEM_COPY, + // VirtualKey.OEM_AUTO, + // VirtualKey.OEM_ENLW, + // VirtualKey.OEM_BACKTAB, + // VirtualKey.ATTN, + // VirtualKey.CRSEL, + // VirtualKey.EXSEL, + // VirtualKey.EREOF, + // VirtualKey.PLAY, + // VirtualKey.ZOOM, + // VirtualKey.NONAME, + // VirtualKey.PA1, + // VirtualKey.OEM_CLEAR, + }; + + internal KeybindManager(Plugin plugin) + { + Plugin = plugin; + Plugin.GameInteropProvider.InitializeFromAttributes(this); + + // Handle keybinds from the game on every tick. + Plugin.Framework.Update += HandleKeybinds; + } + + public void Dispose() + { + Plugin.Framework.Update -= HandleKeybinds; + } + + private void UpdateKeybinds() + { + foreach (var name in KeybindsToIntercept.Keys) + { + Keybinds[name] = GetKeybind(name); + } + } + + private static ModifierFlag GetModifiers(KeyboardSource source) + { + var modifierState = ModifierFlag.None; + if (source == KeyboardSource.Game) + { + if (Plugin.KeyState[VirtualKey.MENU]) + modifierState |= ModifierFlag.Alt; + if (Plugin.KeyState[VirtualKey.CONTROL]) + modifierState |= ModifierFlag.Ctrl; + if (Plugin.KeyState[VirtualKey.SHIFT]) + modifierState |= ModifierFlag.Shift; + return modifierState; + } + + if (ImGui.GetIO().KeyAlt) + modifierState |= ModifierFlag.Alt; + if (ImGui.GetIO().KeyCtrl) + modifierState |= ModifierFlag.Ctrl; + if (ImGui.GetIO().KeyShift) + modifierState |= ModifierFlag.Shift; + + return modifierState; + } + + private static bool KeyPressed(KeyboardSource source, VirtualKey key) + { + if (key == VirtualKey.NO_KEY) + return false; + if (source == KeyboardSource.Game) + return Plugin.KeyState[key]; + + return key.TryToImGui(out var imguiKey) && ImGui.IsKeyPressed(imguiKey); + } + + private static bool ComboPressed(KeyboardSource source, VirtualKey key, ModifierFlag modifier, ModifierFlag? modifierState = null, bool modifiersOnly = false) + { + // When we're in an input, we don't want to process any keybinds that + // don't have a modifier (or only use shift) and are not explicitly + // whitelisted. + if (modifiersOnly && !ModifierlessChatKeys.Contains(key) && modifier is ModifierFlag.None or ModifierFlag.Shift) + return false; + + modifierState ??= GetModifiers(source); + var modifierPressed = Plugin.Config.KeybindMode switch + { + KeybindMode.Strict => modifier == modifierState.Value, + KeybindMode.Flexible => modifierState.Value.HasFlag(modifier), + _ => false + }; + + return KeyPressed(source, key) && modifierPressed; + } + + private static bool ConfigKeybindPressed(KeyboardSource source, ConfigKeyBind? bind, ModifierFlag? modifierState = null, bool modifiersOnly = false) + { + return bind != null && ComboPressed(source, bind.Key, bind.Modifier, modifierState: modifierState, modifiersOnly: modifiersOnly); + } + + private void HandleKeybinds(IFramework _ ) => HandleKeybinds(KeyboardSource.Game); + + internal void HandleKeybinds(KeyboardSource source, bool ignoreChatOpen = false, bool modifiersOnly = false) + { + // Refresh current keybinds every 5s + if (LastRefresh + 5 * 1000 < Environment.TickCount64) + { + UpdateKeybinds(); + DirectChat = Plugin.GameConfig.TryGet(UiControlOption.DirectChat, out bool option) && option; + LastRefresh = Environment.TickCount64; + } + + // Vanilla text input has focus + if (RaptureAtkModule.Instance()->AtkModule.IsTextInputActive()) + return; + + var modifierState = GetModifiers(source); + + // Test for custom keybinds for changing chat tabs before checking + // vanilla keybinds. + if (ConfigKeybindPressed(source, Plugin.Config.ChatTabForward)) + { + Plugin.KeyState[Plugin.Config.ChatTabForward!.Key] = false; + Plugin.ChatLogWindow.ChangeTabDelta(1); + return; + } + if (ConfigKeybindPressed(source, Plugin.Config.ChatTabBackward)) + { + Plugin.KeyState[Plugin.Config.ChatTabBackward!.Key] = false; + Plugin.ChatLogWindow.ChangeTabDelta(-1); + return; + } + + // Only process the active combo with the most modifiers. + var currentBest = (VirtualKey.NO_KEY, "", 0); + foreach (var (toIntercept, keybind) in Keybinds) + { + if (toIntercept is "CMD_CHAT" or "CMD_COMMAND" && (ignoreChatOpen || DirectChat)) + continue; + + void Intercept(VirtualKey vk, ModifierFlag modifier) + { + if (!ComboPressed(source, vk, modifier, modifierState: modifierState, modifiersOnly: modifiersOnly)) + return; + + var bits = BitOperations.PopCount((uint) modifier); + if (bits < currentBest.Item3) + return; + + currentBest = (vk, toIntercept, bits); + } + + Intercept(keybind.Key1, keybind.Modifier1); + Intercept(keybind.Key2, keybind.Modifier2); + } + + if (currentBest.Item1 == VirtualKey.NO_KEY) + return; + + Plugin.KeyState[currentBest.Item1] = false; + if (!KeybindsToIntercept.TryGetValue(currentBest.Item2, out var info)) + return; + + try + { + TellReason? reason = info.Channel == InputChannel.Tell ? TellReason.Reply : null; + Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(info) { TellReason = reason, }); + } + catch (Exception ex) + { + Plugin.Log.Error(ex, "Error in chat Activated event"); + } + } + + private Keybind GetKeybind(string id) + { + var outData = stackalloc byte[11]; + var idString = Utf8String.FromString(id); + GetKeybindNative(UIInputData.Instance(), idString, (nint) outData); + idString->Dtor(true); + + var key1 = RemapInvalidVirtualKey((VirtualKey) outData[0]); + var key2 = RemapInvalidVirtualKey((VirtualKey) outData[2]); + return new Keybind + { + Key1 = key1, + Modifier1 = (ModifierFlag) outData[1], + Key2 = key2, + Modifier2 = (ModifierFlag) outData[3], + }; + } + + private static VirtualKey RemapInvalidVirtualKey(VirtualKey key) + { + return key switch + { + VirtualKey.F23 => VirtualKey.OEM_2, // /? + (VirtualKey) 140 => VirtualKey.OEM_7, // '" + _ => key + }; + } +} \ No newline at end of file diff --git a/ChatTwo/Ui/ChatLogWindow.cs b/ChatTwo/Ui/ChatLogWindow.cs index c0d925f..e522c32 100644 --- a/ChatTwo/Ui/ChatLogWindow.cs +++ b/ChatTwo/Ui/ChatLogWindow.cs @@ -3,12 +3,12 @@ using System.Numerics; using System.Runtime.InteropServices; using System.Text; using ChatTwo.Code; +using ChatTwo.GameFunctions; using ChatTwo.GameFunctions.Types; using ChatTwo.Resources; using ChatTwo.Util; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; @@ -121,7 +121,6 @@ public sealed class ChatLogWindow : Window TextCommandSheet = Plugin.DataManager.GetExcelSheet()!; FontIcon = Plugin.TextureProvider.CreateFromTexFile(Plugin.DataManager.GetFile("common/font/fonticon_ps5.tex")!); - Plugin.Functions.Chat.Activated += Activated; Plugin.ClientState.Login += Login; Plugin.ClientState.Logout += Logout; @@ -133,7 +132,6 @@ public sealed class ChatLogWindow : Window Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "ItemDetail", PayloadHandler.MoveTooltip); Plugin.ClientState.Logout -= Logout; Plugin.ClientState.Login -= Login; - Plugin.Functions.Chat.Activated -= Activated; FontIcon?.Dispose(); Plugin.Commands.Register("/chat2").Execute -= ToggleChat; Plugin.Commands.Register("/clearlog2").Execute -= ClearLog; @@ -149,7 +147,7 @@ public sealed class ChatLogWindow : Window Plugin.MessageManager.FilterAllTabsAsync(); } - private void Activated(ChatActivatedArgs args) + internal void Activated(ChatActivatedArgs args) { Activate = true; PlayedClosingSound = false; @@ -337,83 +335,6 @@ public sealed class ChatLogWindow : Window return height; } - private void HandleKeybinds(bool modifiersOnly = false) - { - 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; - - bool KeyPressed(VirtualKey vk, ModifierFlag modifier) - { - if (!vk.TryToImGui(out var key)) - return false; - - var modifierPressed = Plugin.Config.KeybindMode switch - { - KeybindMode.Strict => modifier == modifierState, - KeybindMode.Flexible => modifierState.HasFlag(modifier), - _ => false, - }; - - return ImGui.IsKeyPressed(key) && modifierPressed && (modifier != 0 || !modifiersOnly); - } - - // Test for custom keybinds for changing chat tabs before checking - // vanilla keybinds. - if (Plugin.Config.ChatTabBackward != null && KeyPressed(Plugin.Config.ChatTabBackward.Key, Plugin.Config.ChatTabBackward.Modifier)) - { - Plugin.ChatLogWindow.ChangeTabDelta(-1); - return; - } - if (Plugin.Config.ChatTabForward != null && KeyPressed(Plugin.Config.ChatTabForward.Key, Plugin.Config.ChatTabForward.Modifier)) - { - Plugin.ChatLogWindow.ChangeTabDelta(1); - return; - } - - var turnedOff = new Dictionary(); - foreach (var (toIntercept, keybind) in Plugin.Functions.Chat.Keybinds) - { - if (toIntercept is "CMD_CHAT" or "CMD_COMMAND") - continue; - - void Intercept(VirtualKey vk, ModifierFlag modifier) - { - if (!KeyPressed(vk, modifier)) - return; - - var bits = BitOperations.PopCount((uint) modifier); - if (!turnedOff.TryGetValue(vk, out var previousBits) || previousBits.Item1 < bits) - turnedOff[vk] = ((uint) bits, toIntercept); - } - - Intercept(keybind.Key1, keybind.Modifier1); - Intercept(keybind.Key2, keybind.Modifier2); - } - - foreach (var (_, (_, keybind)) in turnedOff) - { - if (!GameFunctions.Chat.KeybindsToIntercept.TryGetValue(keybind, out var info)) - continue; - - try - { - TellReason? reason = info.Channel == InputChannel.Tell ? TellReason.Reply : null; - Activated(new ChatActivatedArgs(info) { TellReason = reason, }); - } - catch (Exception ex) - { - Plugin.Log.Error(ex, "Error in chat Activated event"); - } - } - } - internal void ChangeTab(int index) => WantedTab = index; internal void ChangeTabDelta(int offset) @@ -801,9 +722,10 @@ public sealed class ChatLogWindow : Window } } + // Process keybinds that have modifiers while the chat is focused. if (ImGui.IsItemActive()) { - HandleKeybinds(true); + Plugin.Functions.KeybindManager.HandleKeybinds(KeyboardSource.ImGui, true, true); LastActivityTime = FrameTime; }