chore: refactor keybinds to be in new KeybindManager

This commit is contained in:
Dean Sheather
2024-07-15 19:16:30 +10:00
parent 352088dfed
commit fb167a8161
4 changed files with 494 additions and 315 deletions
+5 -233
View File
@@ -1,15 +1,11 @@
using System.Numerics; using System.Text;
using System.Text;
using ChatTwo.Code; using ChatTwo.Code;
using ChatTwo.GameFunctions.Types; using ChatTwo.GameFunctions.Types;
using ChatTwo.Resources; using ChatTwo.Resources;
using ChatTwo.Util; using ChatTwo.Util;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Config;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Memory; using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Application.Network; using FFXIVClientStructs.FFXIV.Application.Network;
using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.Framework;
@@ -29,10 +25,6 @@ namespace ChatTwo.GameFunctions;
internal sealed unsafe class Chat : IDisposable internal sealed unsafe class Chat : IDisposable
{ {
// Functions // Functions
// Replace with <https://github.com/aers/FFXIVClientStructs/pull/1036>
[Signature("E8 ?? ?? ?? ?? 48 8D 4D A0 8B F8")]
private readonly delegate* unmanaged<UIInputData*, Utf8String*, nint, uint> GetKeybindNative = null!;
[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D B9 ?? ?? ?? ?? 33 C0")] [Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8D B9 ?? ?? ?? ?? 33 C0")]
private readonly delegate* unmanaged<RaptureLogModule*, ushort, Utf8String*, Utf8String*, ulong, ulong, ushort, byte, int, byte, void> PrintTellNative = null!; private readonly delegate* unmanaged<RaptureLogModule*, ushort, Utf8String*, Utf8String*, ulong, ulong, ushort, byte, int, byte, void> PrintTellNative = null!;
@@ -54,11 +46,6 @@ internal sealed unsafe class Chat : IDisposable
[Signature("48 8D 35 ?? ?? ?? ?? 8B 05", ScanType = ScanType.StaticAddress)] [Signature("48 8D 35 ?? ?? ?? ?? 8B 05", ScanType = ScanType.StaticAddress)]
private readonly char* CurrentCharacter = null!; private readonly char* CurrentCharacter = null!;
// Events
internal event ChatActivatedEventDelegate? Activated;
internal delegate void ChatActivatedEventDelegate(ChatActivatedArgs args);
private Plugin Plugin { get; } private Plugin Plugin { get; }
/// <summary> /// <summary>
@@ -70,9 +57,6 @@ internal sealed unsafe class Chat : IDisposable
internal bool UsesTellTempChannel { get; set; } internal bool UsesTellTempChannel { get; set; }
internal InputChannel? PreviousChannel { get; private set; } internal InputChannel? PreviousChannel { get; private set; }
private bool DirectChat;
private long LastRefresh;
internal Chat(Plugin plugin) internal Chat(Plugin plugin)
{ {
Plugin = plugin; Plugin = plugin;
@@ -92,7 +76,6 @@ internal sealed unsafe class Chat : IDisposable
// EurekaContextMenuTellHook = Plugin.GameInteropProvider.HookFromAddress<RaptureShellModule.Delegates.SetContextTellTargetInForay>(RaptureShellModule.MemberFunctionPointers.SetContextTellTargetInForay, SetContextTellTargetInForay); // EurekaContextMenuTellHook = Plugin.GameInteropProvider.HookFromAddress<RaptureShellModule.Delegates.SetContextTellTargetInForay>(RaptureShellModule.MemberFunctionPointers.SetContextTellTargetInForay, SetContextTellTargetInForay);
// EurekaContextMenuTellHook.Enable(); // EurekaContextMenuTellHook.Enable();
Plugin.Framework.Update += InterceptKeybinds;
Plugin.ClientState.Login += Login; Plugin.ClientState.Login += Login;
Login(); Login();
} }
@@ -100,15 +83,12 @@ internal sealed unsafe class Chat : IDisposable
public void Dispose() public void Dispose()
{ {
Plugin.ClientState.Login -= Login; Plugin.ClientState.Login -= Login;
Plugin.Framework.Update -= InterceptKeybinds;
SetChatLogTellTargetHook?.Dispose(); SetChatLogTellTargetHook?.Dispose();
ReplyInSelectedChatModeHook?.Dispose(); ReplyInSelectedChatModeHook?.Dispose();
ChangeChannelNameHook?.Dispose(); ChangeChannelNameHook?.Dispose();
ChatLogRefreshHook?.Dispose(); ChatLogRefreshHook?.Dispose();
EurekaContextMenuTellHook?.Dispose(); EurekaContextMenuTellHook?.Dispose();
Activated = null;
} }
internal string? GetLinkshellName(uint idx) internal string? GetLinkshellName(uint idx)
@@ -164,182 +144,6 @@ internal sealed unsafe class Chat : IDisposable
return 0xFF | (rgb << 8); return 0xFF | (rgb << 8);
} }
private readonly Dictionary<string, Keybind> _keybinds = new();
internal IReadOnlyDictionary<string, Keybind> Keybinds => _keybinds;
internal static readonly IReadOnlyDictionary<string, ChannelSwitchInfo> KeybindsToIntercept = new Dictionary<string, ChannelSwitchInfo>
{
["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<ModifierFlag>())
{
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<VirtualKey, (uint, string)>();
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() private void Login()
{ {
var agent = AgentChatLog.Instance(); 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)) if (eventId != 0x31 || value == null || value->UInt is not (0x05 or 0x0C))
return ChatLogRefreshHook!.Original(log, eventId, value); return ChatLogRefreshHook!.Original(log, eventId, value);
if (DirectChat && CurrentCharacter != null) if (Plugin.Functions.KeybindManager.DirectChat && CurrentCharacter != null)
{ {
// FIXME: this whole system sucks // FIXME: this whole system sucks
// FIXME v2: I hate everything about this, but it works // FIXME v2: I hate everything about this, but it works
@@ -376,7 +180,7 @@ internal sealed unsafe class Chat : IDisposable
try try
{ {
Activated?.Invoke(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { Input = input, }); Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { Input = input, });
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -397,7 +201,7 @@ internal sealed unsafe class Chat : IDisposable
try try
{ {
Activated?.Invoke(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { AddIfNotPresent = addIfNotPresent, }); Plugin.ChatLogWindow.Activated(new ChatActivatedArgs(new ChannelSwitchInfo(null)) { AddIfNotPresent = addIfNotPresent, });
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -463,7 +267,7 @@ internal sealed unsafe class Chat : IDisposable
try try
{ {
var target = new TellTarget(playerName->ToString(), worldId, contentId, (TellReason) reason); 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, TellReason = (TellReason) reason,
TellTarget = target, TellTarget = target,
@@ -547,38 +351,6 @@ internal sealed unsafe class Chat : IDisposable
utfWorld->Dtor(true); 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) internal TellHistoryInfo? GetTellHistoryInfo(int index)
{ {
var acquaintance = AcquaintanceModule.Instance()->GetTellHistory(index); var acquaintance = AcquaintanceModule.Instance()->GetTellHistory(index);
+3
View File
@@ -25,11 +25,13 @@ internal unsafe class GameFunctions : IDisposable
#endregion #endregion
private Plugin Plugin { get; } private Plugin Plugin { get; }
internal KeybindManager KeybindManager { get; }
internal Chat Chat { get; } internal Chat Chat { get; }
internal GameFunctions(Plugin plugin) internal GameFunctions(Plugin plugin)
{ {
Plugin = plugin; Plugin = plugin;
KeybindManager = new KeybindManager(plugin);
Chat = new Chat(Plugin); Chat = new Chat(Plugin);
Plugin.GameInteropProvider.InitializeFromAttributes(this); Plugin.GameInteropProvider.InitializeFromAttributes(this);
@@ -40,6 +42,7 @@ internal unsafe class GameFunctions : IDisposable
public void Dispose() public void Dispose()
{ {
Chat.Dispose(); Chat.Dispose();
KeybindManager.Dispose();
ResolveTextCommandPlaceholderHook?.Dispose(); ResolveTextCommandPlaceholderHook?.Dispose();
+482
View File
@@ -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 <https://github.com/aers/FFXIVClientStructs/pull/1036>
[Signature("E8 ?? ?? ?? ?? 48 8D 4D A0 8B F8")]
private readonly delegate* unmanaged<UIInputData*, Utf8String*, nint, uint> GetKeybindNative = null!;
private Plugin Plugin { get; }
internal bool DirectChat;
private long LastRefresh;
private readonly Dictionary<string, Keybind> Keybinds = new();
private static readonly IReadOnlyDictionary<string, ChannelSwitchInfo> KeybindsToIntercept = new Dictionary<string, ChannelSwitchInfo>
{
["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<VirtualKey> 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
};
}
}
+4 -82
View File
@@ -3,12 +3,12 @@ using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using ChatTwo.Code; using ChatTwo.Code;
using ChatTwo.GameFunctions;
using ChatTwo.GameFunctions.Types; using ChatTwo.GameFunctions.Types;
using ChatTwo.Resources; using ChatTwo.Resources;
using ChatTwo.Util; using ChatTwo.Util;
using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface; using Dalamud.Interface;
@@ -121,7 +121,6 @@ public sealed class ChatLogWindow : Window
TextCommandSheet = Plugin.DataManager.GetExcelSheet<TextCommand>()!; TextCommandSheet = Plugin.DataManager.GetExcelSheet<TextCommand>()!;
FontIcon = Plugin.TextureProvider.CreateFromTexFile(Plugin.DataManager.GetFile<TexFile>("common/font/fonticon_ps5.tex")!); FontIcon = Plugin.TextureProvider.CreateFromTexFile(Plugin.DataManager.GetFile<TexFile>("common/font/fonticon_ps5.tex")!);
Plugin.Functions.Chat.Activated += Activated;
Plugin.ClientState.Login += Login; Plugin.ClientState.Login += Login;
Plugin.ClientState.Logout += Logout; Plugin.ClientState.Logout += Logout;
@@ -133,7 +132,6 @@ public sealed class ChatLogWindow : Window
Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "ItemDetail", PayloadHandler.MoveTooltip); Plugin.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "ItemDetail", PayloadHandler.MoveTooltip);
Plugin.ClientState.Logout -= Logout; Plugin.ClientState.Logout -= Logout;
Plugin.ClientState.Login -= Login; Plugin.ClientState.Login -= Login;
Plugin.Functions.Chat.Activated -= Activated;
FontIcon?.Dispose(); FontIcon?.Dispose();
Plugin.Commands.Register("/chat2").Execute -= ToggleChat; Plugin.Commands.Register("/chat2").Execute -= ToggleChat;
Plugin.Commands.Register("/clearlog2").Execute -= ClearLog; Plugin.Commands.Register("/clearlog2").Execute -= ClearLog;
@@ -149,7 +147,7 @@ public sealed class ChatLogWindow : Window
Plugin.MessageManager.FilterAllTabsAsync(); Plugin.MessageManager.FilterAllTabsAsync();
} }
private void Activated(ChatActivatedArgs args) internal void Activated(ChatActivatedArgs args)
{ {
Activate = true; Activate = true;
PlayedClosingSound = false; PlayedClosingSound = false;
@@ -337,83 +335,6 @@ public sealed class ChatLogWindow : Window
return height; 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<VirtualKey, (uint, string)>();
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 ChangeTab(int index) => WantedTab = index;
internal void ChangeTabDelta(int offset) 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()) if (ImGui.IsItemActive())
{ {
HandleKeybinds(true); Plugin.Functions.KeybindManager.HandleKeybinds(KeyboardSource.ImGui, true, true);
LastActivityTime = FrameTime; LastActivityTime = FrameTime;
} }