From 4ad07f429ad5ae4507a48712d66f7a1675583721 Mon Sep 17 00:00:00 2001 From: Mino Date: Fri, 3 Mar 2023 12:40:12 +0900 Subject: [PATCH 1/3] Block inputs from being delivered on `WantCaptureKeyboard` --- Dalamud/Game/Gui/GameGui.cs | 3 +- .../Interface/Internal/InterfaceManager.cs | 44 +++++++++++++++---- lib/ImGuiScene | 2 +- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index d3b4796427..33169d6014 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -575,8 +575,9 @@ private IntPtr ToggleUiHideDetour(IntPtr thisPtr, byte unknownByte) private char HandleImmDetour(IntPtr framework, char a2, byte a3) { + var io = ImGui.GetIO(); var result = this.handleImmHook.Original(framework, a2, a3); - return ImGui.GetIO().WantTextInput + return (io.WantTextInput || io.WantCaptureKeyboard) ? (char)0 : result; } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 2d4b979ddd..a42317ffc7 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -217,6 +217,7 @@ public bool OverrideGameCursor /// public void Dispose() { + this.framework.Update -= OnFrameworkUpdate; this.framework.RunOnFrameworkThread(() => { this.setCursorHook.Dispose(); @@ -980,6 +981,8 @@ private unsafe void SetupFonts() [ServiceManager.CallWhenServicesReady] private void ContinueConstruction(SigScanner sigScanner, Framework framework) { + framework.Update += this.OnFrameworkUpdate; + this.address.Setup(sigScanner); framework.RunOnFrameworkThread(() => { @@ -1020,6 +1023,7 @@ private void ContinueConstruction(SigScanner sigScanner, Framework framework) }); } + // This is intended to only be called as a handler attached to scene.OnNewRenderFrame private void RebuildFontsInternal() { @@ -1090,23 +1094,45 @@ private IntPtr SetCursorDetour(IntPtr hCursor) return this.setCursorHook.IsDisposed ? User32.SetCursor(new User32.SafeCursorHandle(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor); } - private void OnNewInputFrame() + private void OnFrameworkUpdate(Framework framework) { - var dalamudInterface = Service.GetNullable(); - var gamepadState = Service.GetNullable(); - var keyState = Service.GetNullable(); - - if (dalamudInterface == null || gamepadState == null || keyState == null) - return; - + var keyState = Service.Get(); + var io = ImGui.GetIO(); + // fix for keys in game getting stuck, if you were holding a game key (like run) // and then clicked on an imgui textbox - imgui would swallow the keyup event, // so the game would think the key remained pressed continuously until you left // imgui and pressed and released the key again - if (ImGui.GetIO().WantTextInput) + // + // Notes: + // Simplified main game loop (names based on FFXIVClientStructs) + // +--> DispatchMessage --> .. --> Framework_Tick --> .. --> Framework_TaskUpdateInputDevice --> .. --> Present --+ + // | (Updates KeyState) (this fn) (Uses KeyState) | + // +--------------------------------------------------------------------------------------------------------------+ + // - So this "fix" must be applied before `Framework_TaskUpdateInputDevice` (check FFXIVClientStructs) is called + // or else any KeyPressed events raised during the current frame will erronously be applied for a single frame. + // - Currently we naively clear all `KeyState` buffer to zero to simulate key releases. This approach may cause some + // problem as original values won't be restored (because we don't know what value should be at this point) + // even after ImGui no longer wants to capture keyboard inputs. (in practice, player can see this in effect when + // an action like moving/running being interrupted out of nowhere and they have to press WASD again) + // - This inconvenience can be solved if we introduce something like an "overlay" which tracks inputs and restores + // `KeyState` to what should've been once `WantCaptureKeyboard` is released. So, what we have right now is an + // interim solution in the meantime. + + if (io.WantCaptureKeyboard || io.WantTextInput) { keyState.ClearAll(); } + } + + private void OnNewInputFrame() + { + var dalamudInterface = Service.GetNullable(); + var gamepadState = Service.GetNullable(); + var keyState = Service.GetNullable(); + + if (dalamudInterface == null || gamepadState == null || keyState == null) + return; // TODO: mouse state? diff --git a/lib/ImGuiScene b/lib/ImGuiScene index 262d3b0668..f287caa6c4 160000 --- a/lib/ImGuiScene +++ b/lib/ImGuiScene @@ -1 +1 @@ -Subproject commit 262d3b0668196fb236e2191c4a37e9be94e5a7a3 +Subproject commit f287caa6c4a67f3f1fd3b34798e02939ef749360 From d8f6de55d60262dc91ea58a1022ac66dcdecfd39 Mon Sep 17 00:00:00 2001 From: Mino Date: Fri, 3 Mar 2023 15:30:54 +0900 Subject: [PATCH 2/3] Add KeyCapture service. Polling `KeyState` buffer and writing zeros as needed was common method to implement hotkey feature. This, however, can cause certain actions like running being abruptly interrupted or misbehaving (processing as if it was pressed again despite the user never released it) due to the nature of key state buffer. This commit introduces `SimulatedKeyState` and `KeyCapture` services. `SimulatedKeyStates` roughly simulates how the game calculates key states and `KeyCapture` uses their own rules to decide when to pass this value as inputs to the game. --- .../Game/ClientState/Keys/InputDevicePoll.cs | 65 ++++++++ .../Game/ClientState/Keys/KeyCapture.State.cs | 14 ++ Dalamud/Game/ClientState/Keys/KeyCapture.cs | 152 ++++++++++++++++++ Dalamud/Game/ClientState/Keys/KeyStateFlag.cs | 12 ++ .../Game/ClientState/Keys/KeyStateIndex.cs | 68 ++++++++ Dalamud/Game/ClientState/Keys/RawKeyState.cs | 89 ++++++++++ .../ClientState/Keys/SimulatedKeyState.cs | 139 ++++++++++++++++ .../ClientState/Keys/VirtualKeyExtensions.cs | 10 ++ .../Interface/Internal/InterfaceManager.cs | 56 +++++-- Dalamud/Utility/SpanExtensions.cs | 67 ++++++++ 10 files changed, 658 insertions(+), 14 deletions(-) create mode 100644 Dalamud/Game/ClientState/Keys/InputDevicePoll.cs create mode 100644 Dalamud/Game/ClientState/Keys/KeyCapture.State.cs create mode 100644 Dalamud/Game/ClientState/Keys/KeyCapture.cs create mode 100644 Dalamud/Game/ClientState/Keys/KeyStateFlag.cs create mode 100644 Dalamud/Game/ClientState/Keys/KeyStateIndex.cs create mode 100644 Dalamud/Game/ClientState/Keys/RawKeyState.cs create mode 100644 Dalamud/Game/ClientState/Keys/SimulatedKeyState.cs create mode 100644 Dalamud/Utility/SpanExtensions.cs diff --git a/Dalamud/Game/ClientState/Keys/InputDevicePoll.cs b/Dalamud/Game/ClientState/Keys/InputDevicePoll.cs new file mode 100644 index 0000000000..0e0feec920 --- /dev/null +++ b/Dalamud/Game/ClientState/Keys/InputDevicePoll.cs @@ -0,0 +1,65 @@ +using System; +using System.Runtime.InteropServices; + +using Dalamud.Hooking; +using Dalamud.Utility; + +namespace Dalamud.Game.ClientState.Keys; + +/// +/// This class provides events related to game inputs. +/// +[ServiceManager.BlockingEarlyLoadedService] +internal sealed class InputDevicePoll : IDisposable, IServiceType +{ + private readonly Hook updateInputHook; + + /// + /// Raised when the game is about to poll inputs. + /// + public event Action? OnBeforePoll; + + /// + /// Raised when the game polled inputs. + /// + public event Action? OnAfterPoll; + + [ServiceManager.ServiceConstructor] + public InputDevicePoll(SigScanner sigScanner) + { + // Client::System::Framework::Framework_TaskUpdateInputDevice (names from FFXIVClientStructs) calls this function + // 48:83EC 48 | sub rsp,48 | + // 48:895C24 50 | mov qword ptr ss: [rsp+50],rbx | + // BA 1E000000 | mov edx,1E | + // 48:897424 58 | mov qword ptr ss: [rsp+58],rsi | + // 48:897C24 40 | mov qword ptr ss: [rsp+40],rdi | + // 48:8BF9 | mov rdi,rcx | + // 48:81C1 60040000 | add rcx,460 | + // 0F297424 30 | movaps xmmword ptr ss: [rsp+30],xmm6 | + // 0F28F1 | movaps xmm6,xmm1 | + // E8 0158FDFF | call ffxiv_dx11.7FF6CDD89970 | + var updateInputAddr = + sigScanner.ScanText( + "48?????? 48???????? BA ????0000 48???????? 48???????? 48???? 4881C1????0000 0F???????? 0F???? E8"); + this.updateInputHook = Hook.FromAddress(updateInputAddr, this.OnUpdateInput); + this.updateInputHook.Enable(); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate nint UpdateInputDelegate(nint a1, nint a2); + + /// + public void Dispose() + { + this.updateInputHook.Dispose(); + } + + private nint OnUpdateInput(nint a1, nint a2) + { + this.OnBeforePoll?.InvokeSafely(); + var ret = this.updateInputHook.Original(a1, a2); + this.OnAfterPoll?.InvokeSafely(); + + return ret; + } +} diff --git a/Dalamud/Game/ClientState/Keys/KeyCapture.State.cs b/Dalamud/Game/ClientState/Keys/KeyCapture.State.cs new file mode 100644 index 0000000000..be781789ff --- /dev/null +++ b/Dalamud/Game/ClientState/Keys/KeyCapture.State.cs @@ -0,0 +1,14 @@ +using System; + +namespace Dalamud.Game.ClientState.Keys; + +partial class KeyCapture +{ + [Flags] + private enum State : byte + { + CaptureAll = 0x01, + CaptureAllSingleFrame = 0x02, + RestoreAllOnNextFrame = 0x04, + } +} diff --git a/Dalamud/Game/ClientState/Keys/KeyCapture.cs b/Dalamud/Game/ClientState/Keys/KeyCapture.cs new file mode 100644 index 0000000000..ef26bc4756 --- /dev/null +++ b/Dalamud/Game/ClientState/Keys/KeyCapture.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; + +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using MonoMod.Utils; + +namespace Dalamud.Game.ClientState.Keys; + +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.BlockingEarlyLoadedService] +public sealed partial class KeyCapture : IDisposable, IServiceType +{ + private readonly InputDevicePoll mInputDevicePoll; + + private readonly SimulatedKeyState mSimulatedKeyState; + + private readonly RawKeyState mRawKeyState; + + private readonly List mNextCaptureSet = new(); + + private readonly List mNextRestoreSet = new(); + + private State mCaptureState; + + [ServiceManager.ServiceConstructor] + private KeyCapture(InputDevicePoll devicePoll, SimulatedKeyState simulatedKeyState, RawKeyState rawKeyState) + { + this.mInputDevicePoll = devicePoll; + this.mSimulatedKeyState = simulatedKeyState; + this.mRawKeyState = rawKeyState; + + this.mInputDevicePoll.OnBeforePoll += this.OnBeforeInputPoll; + } + + /// + void IDisposable.Dispose() + { + this.mInputDevicePoll.OnBeforePoll -= this.OnBeforeInputPoll; + } + + /// + /// Stop delivering inputs to the game indefinitely. + /// + /// + /// If this is set to true then it will block all inputs. + /// To stop capturing, set this to false. + /// + public void CaptureAll(bool doCapture = true) + { + this.mCaptureState = doCapture switch + { + true => this.mCaptureState | State.CaptureAll, + false => this.mCaptureState & ~State.CaptureAll, + }; + + if (!doCapture) + { + // Also queue restore so that the game can receive held inputs again. + this.mCaptureState |= State.RestoreAllOnNextFrame; + } + } + + /// + /// Captures all keys for a single frame. + /// + public void CaptureAllSingleFrame() + { + this.mCaptureState |= State.CaptureAllSingleFrame; + } + + /// + /// Captures a designated key for a single frame. + /// + /// A virtual key code to capture. + public void CaptureSingleFrame(ushort vkCode) + { + this.mNextCaptureSet.Add(vkCode); + } + + private void OnBeforeInputPoll() + { + // Restore all keys + if (this.mCaptureState.HasFlag(State.RestoreAllOnNextFrame)) + { + // Remove pending restore flag + this.mCaptureState &= ~State.RestoreAllOnNextFrame; + + // Copy all simulated key states into actual buffer. + // Note that simulated key state is laid out very carefully so that + // it could just be memcpy'd in this situation. + this.mSimulatedKeyState.RawState.CopyTo(this.mRawKeyState.RawState); + + // If restored all keys, there's no point of restoring individual keys. + this.mNextRestoreSet.Clear(); + } + + // Restore individual keys + foreach (var vkCode in this.mNextRestoreSet) + { + if (!this.mSimulatedKeyState.TryGetState(vkCode, out var state)) + { + continue; + } + + // Set key state to its original value + // Log.Verbose("restore: {VkCode} = {State}" vkCode, state); + this.mRawKeyState.SetState(vkCode, state); + } + + this.mNextRestoreSet.Clear(); + + // Capture key states. + // Note that we process capturing **only after** finished restoring keys. + // This allows capturing to take higher precedence over restoring if they're queued on the same frame. + if (this.mCaptureState.HasFlag(State.CaptureAll)) + { + // We clear game inputs here on every frame to block delivering any new inputs to the game + // because we didn't at DispatchMessage time. + // + // Thankfully zeroing ~1KB contiguous memory is very fast so we don't even need to touch WndProc hook at all. + this.mRawKeyState.RawState.Clear(); + + // If we captured all keys, there's no point of restoring individual keys. + this.mNextCaptureSet.Clear(); + } + + if (this.mCaptureState.Has(State.CaptureAllSingleFrame)) + { + // Remove CaptureAllSingleFrame and Add RestoreAllOnNextFrame + this.mCaptureState = (this.mCaptureState | State.RestoreAllOnNextFrame) & + ~State.CaptureAllSingleFrame; + this.mRawKeyState.RawState.Clear(); + } + + // Capture individual keys + foreach (var vkCode in this.mNextCaptureSet) + { + if (!VirtualKeyExtensions.IsValidVirtualKey(vkCode)) + { + continue; + } + + // Release a key + this.mRawKeyState.SetState(vkCode, KeyStateFlag.None); + this.mNextRestoreSet.Add(vkCode); + } + + this.mNextCaptureSet.Clear(); + } +} diff --git a/Dalamud/Game/ClientState/Keys/KeyStateFlag.cs b/Dalamud/Game/ClientState/Keys/KeyStateFlag.cs new file mode 100644 index 0000000000..12226a9987 --- /dev/null +++ b/Dalamud/Game/ClientState/Keys/KeyStateFlag.cs @@ -0,0 +1,12 @@ +using System; + +namespace Dalamud.Game.ClientState.Keys; + +[Flags] +public enum KeyStateFlag +{ + None = 0x00, + Down = 0x01, + JustPressed = 0x02, + JustReleased = 0x04, +} diff --git a/Dalamud/Game/ClientState/Keys/KeyStateIndex.cs b/Dalamud/Game/ClientState/Keys/KeyStateIndex.cs new file mode 100644 index 0000000000..09e6aebfc0 --- /dev/null +++ b/Dalamud/Game/ClientState/Keys/KeyStateIndex.cs @@ -0,0 +1,68 @@ +using System; +using System.Runtime.InteropServices; + +using Dalamud.Utility; +using Serilog; + +namespace Dalamud.Game.ClientState.Keys; + +// TODO: this is probably safe to expose to plugins without major concerns +// [PluginInterface] +[ServiceManager.BlockingEarlyLoadedService] +internal sealed class KeyStateIndex : IServiceType +{ + private const int InvalidKeyCode = 0; + + private readonly unsafe byte* mVkIndex; + + [ServiceManager.ServiceConstructor] + private unsafe KeyStateIndex(SigScanner sigScanner, ClientState clientState) + { + var moduleBaseAddress = sigScanner.Module.BaseAddress; + var addressResolver = clientState.AddressResolver; + + this.mVkIndex = (byte*)(moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardStateIndexArray)); + this.MaxValidKeyCode = this.AsSpan().Max(); + + Log.Verbose("VkIndex@{Address:X8}h (max={Max:X2}h)", (long)(nint)this.mVkIndex, this.MaxValidKeyCode); + } + + /// + /// Gets the upper bound for the key code. + /// + public byte MaxValidKeyCode { get; } + + /// + /// Translates a virtual key code from Windows into a key code which the game internally uses. + /// + /// A virtual key to translate. + /// A reference to keyCode to receive the translated key code. + /// + /// Returns true if this function successfully reads the key code, false otherwise. + /// + /// + /// If this function returns false then the state of will be unspecified. + /// + public bool TryGetKeyCode(ushort vkCode, out byte keyCode) + { + unsafe + { + keyCode = default; + + if (!VirtualKeyExtensions.IsValidVirtualKey(vkCode)) + { + return false; + } + + keyCode = this.mVkIndex[vkCode]; + if (keyCode == InvalidKeyCode) + { + return false; + } + + return true; + } + } + + private unsafe ReadOnlySpan AsSpan() => new(this.mVkIndex, VirtualKeyExtensions.MaxValidCode + 1); +} diff --git a/Dalamud/Game/ClientState/Keys/RawKeyState.cs b/Dalamud/Game/ClientState/Keys/RawKeyState.cs new file mode 100644 index 0000000000..9c6b668aed --- /dev/null +++ b/Dalamud/Game/ClientState/Keys/RawKeyState.cs @@ -0,0 +1,89 @@ +using System; +using System.Runtime.InteropServices; + +using Serilog; + +namespace Dalamud.Game.ClientState.Keys; + +/// +/// Exposes raw key states read by the game. +/// +/// +/// Unlike this is actually writeable and thus should never be exposed to plugins. +/// +[ServiceManager.BlockingEarlyLoadedService] +internal sealed class RawKeyState : IServiceType +{ + private readonly KeyStateIndex mKeyIndex; + + private readonly unsafe KeyStateFlag* mState; + private readonly int mStateLength; + + [ServiceManager.ServiceConstructor] + private unsafe RawKeyState(SigScanner sigScanner, ClientState clientState, KeyStateIndex vkIndex) + { + this.mKeyIndex = vkIndex; + + var moduleBaseAddress = sigScanner.Module.BaseAddress; + var addressResolver = clientState.AddressResolver; + this.mState = (KeyStateFlag*)(moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardState)); + this.mStateLength = vkIndex.MaxValidKeyCode; + + Log.Verbose("KeyState->Buffer@{Address:X8}h", (long)(nint)this.mState); + } + + /// + /// Gets key state for all keys. + /// + // Note that MaxValidCode is inclusive (that is, actual length should be max+1) + public unsafe Span RawState => new(this.mState, this.mStateLength + 1); + + /// + /// Sets a key state for a given virtual key. + /// + /// The virtual key code to update. + /// The key state to set. + /// + /// No-op if is invalid. + /// + public void SetState(ushort vkCode, KeyStateFlag state) + { + unsafe + { + if (!this.mKeyIndex.TryGetKeyCode(vkCode, out var keyCode)) + { + return; + } + + this.mState[keyCode] = state; + } + } + + /// + /// Gets state value for the key. + /// + /// + /// The virtual key code to retrieve. + /// + /// + /// If this function returns false, the state of this value is considered undefined. + /// + /// + /// Returns true if the virtual key is invalid, false otherwise. + /// + public bool TryGetState(ushort vkCode, out KeyStateFlag state) + { + unsafe + { + state = default; + + if (this.mKeyIndex.TryGetKeyCode(vkCode, out var keyCode)) + { + return false; + } + + state = this.mState[keyCode]; + return true; + } + } +} diff --git a/Dalamud/Game/ClientState/Keys/SimulatedKeyState.cs b/Dalamud/Game/ClientState/Keys/SimulatedKeyState.cs new file mode 100644 index 0000000000..5f258901e6 --- /dev/null +++ b/Dalamud/Game/ClientState/Keys/SimulatedKeyState.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; + +using Serilog; + +namespace Dalamud.Game.ClientState.Keys; + +/// +/// This service emulates key states based on inputs. +/// +[ServiceManager.BlockingEarlyLoadedService] +internal sealed class SimulatedKeyState : IServiceType, IDisposable +{ + private readonly InputDevicePoll mInputDevicePoll; + + private readonly KeyStateIndex mKeyIndex; + + private readonly KeyStateFlag[] mRawState; + + // This is based on the assumption that inputs don't change that much (maybe 1 or 2 per frame) on a single frame. + private readonly List mNextClearSet = new(); + + [ServiceManager.ServiceConstructor] + private SimulatedKeyState(InputDevicePoll devicePoll, KeyStateIndex keyIndex) + { + this.mInputDevicePoll = devicePoll; + this.mKeyIndex = keyIndex; + this.mRawState = new KeyStateFlag[keyIndex.MaxValidKeyCode + 1]; + + this.mInputDevicePoll.OnAfterPoll += this.OnAfterInputPoll; + } + + /// + /// Gets or sets a value indicating whether the main window has focus. + /// + /// + /// This value is used to release all keys when the window doesn't have focus. + /// + public bool HasFocus { get; set; } = true; + + /// + /// Gets the simulated key state for all keys. + /// + /// + /// The order of keys is same as the game so that it just could be directly mem-copied into RawKeyState buffer. + /// + public ReadOnlySpan RawState => this.mRawState; + + /// + void IDisposable.Dispose() + { + this.mInputDevicePoll.OnAfterPoll -= this.OnAfterInputPoll; + } + + /// + /// Reports a key stroke. + /// + /// A virtual key code. + /// True if the key is being held down. otherwise false. + /// + /// This function will be no-op if is invalid. + /// + internal void AddKeyEvent(ushort vkCode, bool down) + { + if (!this.mKeyIndex.TryGetKeyCode(vkCode, out var keyCode)) + { + return; + } + + ref var state = ref this.mRawState[keyCode]; + + // Calculates a state value for the key stroke + if (down) + { + // Key is pressed + if (state.HasFlag(KeyStateFlag.Down)) + { + // nothing to do if the key is already being pressed + return; + } + + // Add Down and JustPressed + state |= KeyStateFlag.Down | KeyStateFlag.JustPressed; + this.mNextClearSet.Add(keyCode); + } + else + { + // Key is released + if (!state.HasFlag(KeyStateFlag.Down)) + { + // nothing to do if the key is already released + return; + } + + // Remove Down and add JustReleased flag. + state = (state & ~KeyStateFlag.Down) | KeyStateFlag.JustReleased; + this.mNextClearSet.Add(keyCode); + } + + Log.Verbose("Translated key: (vk={VkCode:X2}h, kc={Key:X2})", vkCode, keyCode); + Log.Verbose("Simulated key state: (vk={VkCode:X2}h, down={Down}, state={State})", vkCode, down, state); + } + + public bool TryGetState(ushort vkCode, out KeyStateFlag state) + { + state = default; + + if (!this.mKeyIndex.TryGetKeyCode(vkCode, out var keyCode)) + { + return false; + } + + state = this.mRawState[keyCode]; + return true; + } + + private void OnAfterInputPoll() + { + // Release all keys if the game is out of focus. + if (!this.HasFocus) + { + // Apparently game also does similar thing btw. + // (rev8555434_2023/02/03_03:27:18 @ 1404A5DF0h) + // Log.Verbose("no focus, clear"); + this.mRawState.AsSpan().Clear(); + } + + // Clear all JustXXX flags. + foreach (var keyCode in this.mNextClearSet) + { + // Remove JustPressed and JustReleased + this.mRawState[keyCode] &= ~(KeyStateFlag.JustPressed | KeyStateFlag.JustReleased); + + // Log.Verbose("Remove {KeyCode:X2} flag, now {Flag}", keyCode, this.mRawState[keyCode]); + } + + this.mNextClearSet.Clear(); + } +} diff --git a/Dalamud/Game/ClientState/Keys/VirtualKeyExtensions.cs b/Dalamud/Game/ClientState/Keys/VirtualKeyExtensions.cs index 22470fe990..2bd723d6db 100644 --- a/Dalamud/Game/ClientState/Keys/VirtualKeyExtensions.cs +++ b/Dalamud/Game/ClientState/Keys/VirtualKeyExtensions.cs @@ -7,6 +7,11 @@ namespace Dalamud.Game.ClientState.Keys; /// public static class VirtualKeyExtensions { + // The array is accessed in a way that this limit doesn't appear to exist + // but there is other state data past this point, and keys beyond here aren't + // generally valid for most things anyway + internal const int MaxValidCode = 0xF0; + /// /// Get the fancy name associated with this key. /// @@ -16,4 +21,9 @@ public static string GetFancyName(this VirtualKey key) { return key.GetAttribute().FancyName; } + + internal static bool IsValidVirtualKey(ushort vkCode) + { + return vkCode <= MaxValidCode; + } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index a42317ffc7..79f89440a5 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1052,6 +1052,8 @@ private unsafe IntPtr DispatchMessageWDetour(ref User32.MSG msg) { if (msg.hwnd == this.GameWindowHandle && this.scene != null) { + this.DispatchMessageDalamud(in msg); + var res = this.scene.ProcessWndProcW(msg.hwnd, msg.message, (void*)msg.wParam, (void*)msg.lParam); if (res != null) return res.Value; @@ -1060,6 +1062,31 @@ private unsafe IntPtr DispatchMessageWDetour(ref User32.MSG msg) return this.dispatchMessageWHook.IsDisposed ? User32.DispatchMessage(ref msg) : this.dispatchMessageWHook.Original(ref msg); } + private void DispatchMessageDalamud(in User32.MSG msg) + { + // Processes window message and call Dalamud services based on it. + // Ideally, this shouldn't be wired here though.. (not to mention this file is already 1K+ lines long!) + // maybe something something refactoring TODO? + switch (msg.message) + { + // Handle keyboard events + case User32.WindowMessage.WM_KEYDOWN: + case User32.WindowMessage.WM_SYSKEYDOWN: + case User32.WindowMessage.WM_KEYUP: + case User32.WindowMessage.WM_SYSKEYUP: + var simulatedKey = Service.GetNullable(); + + var keyDown = msg.message switch + { + User32.WindowMessage.WM_KEYDOWN or User32.WindowMessage.WM_SYSKEYDOWN => true, + _ => false, + }; + + simulatedKey?.AddKeyEvent((ushort)msg.wParam, keyDown); + break; + } + } + private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) { #if DEBUG @@ -1094,11 +1121,12 @@ private IntPtr SetCursorDetour(IntPtr hCursor) return this.setCursorHook.IsDisposed ? User32.SetCursor(new User32.SafeCursorHandle(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor); } - private void OnFrameworkUpdate(Framework framework) + private void OnFrameworkUpdate(Framework _) { - var keyState = Service.Get(); + var simulatedKey = Service.GetNullable(); + var keyCapture = Service.GetNullable(); var io = ImGui.GetIO(); - + // fix for keys in game getting stuck, if you were holding a game key (like run) // and then clicked on an imgui textbox - imgui would swallow the keyup event, // so the game would think the key remained pressed continuously until you left @@ -1110,18 +1138,18 @@ private void OnFrameworkUpdate(Framework framework) // | (Updates KeyState) (this fn) (Uses KeyState) | // +--------------------------------------------------------------------------------------------------------------+ // - So this "fix" must be applied before `Framework_TaskUpdateInputDevice` (check FFXIVClientStructs) is called - // or else any KeyPressed events raised during the current frame will erronously be applied for a single frame. - // - Currently we naively clear all `KeyState` buffer to zero to simulate key releases. This approach may cause some - // problem as original values won't be restored (because we don't know what value should be at this point) - // even after ImGui no longer wants to capture keyboard inputs. (in practice, player can see this in effect when - // an action like moving/running being interrupted out of nowhere and they have to press WASD again) - // - This inconvenience can be solved if we introduce something like an "overlay" which tracks inputs and restores - // `KeyState` to what should've been once `WantCaptureKeyboard` is released. So, what we have right now is an - // interim solution in the meantime. - - if (io.WantCaptureKeyboard || io.WantTextInput) + // or else any KeyPressed events raised during the current frame will erroneously be applied on current frame. + + // Block inputs from being delivered to the game if ImGui wants it. + if ((io.WantCaptureKeyboard || io.WantTextInput) && keyCapture != null) + { + keyCapture.CaptureAllSingleFrame(); + } + + // Update window focus info + if (simulatedKey != null) { - keyState.ClearAll(); + simulatedKey.HasFocus = User32.GetForegroundWindow() == this.GameWindowHandle; } } diff --git a/Dalamud/Utility/SpanExtensions.cs b/Dalamud/Utility/SpanExtensions.cs new file mode 100644 index 0000000000..72902c666d --- /dev/null +++ b/Dalamud/Utility/SpanExtensions.cs @@ -0,0 +1,67 @@ +using System; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Dalamud.Utility; + +/// +/// Provides helper methods for class. +/// +internal static class SpanExtensions +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref readonly T Max(this ReadOnlySpan span) + where T : IComparisonOperators + { + // JIT inlining quirks + static void ThrowEx() + { + throw new ArgumentException("Span is empty", nameof(span)); + } + + if (span.IsEmpty) + { + ThrowEx(); + } + + ref readonly var max = ref span[0]; + + for (var i = 0; i < span.Length; i++) + { + if (max < span[i]) + { + max = ref span[i]; + } + } + + return ref max; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T Max(this Span span) + where T : IComparisonOperators + { + // JIT inlining quirks + static void ThrowEx() + { + throw new ArgumentException("Span is empty", nameof(span)); + } + + if (span.IsEmpty) + { + ThrowEx(); + } + + ref var max = ref span[0]; + + for (var i = 0; i < span.Length; i++) + { + if (max < span[i]) + { + max = ref span[i]; + } + } + + return ref max; + } +} From 6514d25ae5d06a2eb7592f14e6a5caed9c7e22f2 Mon Sep 17 00:00:00 2001 From: Mino Date: Fri, 3 Mar 2023 17:25:27 +0900 Subject: [PATCH 3/3] Change KeyCapture to internal This is a last minute change. I believe there's no harm in exposing this service to plugins, but who knows... --- Dalamud/Game/ClientState/Keys/KeyCapture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/ClientState/Keys/KeyCapture.cs b/Dalamud/Game/ClientState/Keys/KeyCapture.cs index ef26bc4756..aa9f1e8049 100644 --- a/Dalamud/Game/ClientState/Keys/KeyCapture.cs +++ b/Dalamud/Game/ClientState/Keys/KeyCapture.cs @@ -10,7 +10,7 @@ namespace Dalamud.Game.ClientState.Keys; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed partial class KeyCapture : IDisposable, IServiceType +internal sealed partial class KeyCapture : IDisposable, IServiceType { private readonly InputDevicePoll mInputDevicePoll;