In chat write mode the keyboard belongs to the input — typing "swd" must not walk the character — but AUTORUN must keep going (the user can chat while running). - InputDispatcher.IsActionHeld now returns false while WantCaptureKeyboard is set (a focused chat input), the polling-path twin of the existing gate on Fired actions. This SUPERSEDES the old per-frame OnUpdate early-return, which also killed autorun. Gating here instead lets the movement block keep running, so autorun — a separate latched bool ORed into Forward at the call site, not a polled key — survives. Test updated to encode the new contract. - GameWindow: the movement suppress-guard reverts to ImGui-devtools-only (the retail write mode no longer early-returns); wires DefaultTextInput = the chat input (Tab/Enter activation) and Input.Keyboard for clipboard. Drops the one-shot UI-scale diagnostic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
177 lines
7.6 KiB
C#
177 lines
7.6 KiB
C#
using System.Collections.Generic;
|
|
using AcDream.UI.Abstractions.Input;
|
|
using Silk.NET.Input;
|
|
|
|
namespace AcDream.UI.Abstractions.Tests.Input;
|
|
|
|
/// <summary>
|
|
/// K.1b: <see cref="InputDispatcher.IsActionHeld"/> per-frame held-key
|
|
/// polling. Movement (WASD/Shift/Space) needs "is W currently held this
|
|
/// frame" — that's IsActionHeld. The dispatcher's <c>Fired</c> events
|
|
/// drive press/release transitions; <c>IsActionHeld</c> drives the
|
|
/// per-frame <c>MovementInput</c> struct.
|
|
/// </summary>
|
|
public class InputDispatcherIsActionHeldTests
|
|
{
|
|
private static (InputDispatcher dispatcher, FakeKeyboardSource kb, FakeMouseSource mouse, KeyBindings bindings)
|
|
Build()
|
|
{
|
|
var kb = new FakeKeyboardSource();
|
|
var mouse = new FakeMouseSource();
|
|
var bindings = new KeyBindings();
|
|
var dispatcher = new InputDispatcher(kb, mouse, bindings);
|
|
return (dispatcher, kb, mouse, bindings);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsActionHeld_returns_true_while_bound_key_held()
|
|
{
|
|
var (dispatcher, kb, _, bindings) = Build();
|
|
bindings.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward));
|
|
|
|
Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward));
|
|
|
|
kb.EmitKeyDown(Key.W, ModifierMask.None);
|
|
Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward));
|
|
|
|
kb.EmitKeyUp(Key.W, ModifierMask.None);
|
|
Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsActionHeld_returns_false_when_no_binding_for_action()
|
|
{
|
|
var (dispatcher, kb, _, bindings) = Build();
|
|
// No binding for MovementBackup at all.
|
|
bindings.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward));
|
|
|
|
kb.EmitKeyDown(Key.W, ModifierMask.None);
|
|
|
|
Assert.False(dispatcher.IsActionHeld(InputAction.MovementBackup));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsActionHeld_modifier_mismatch_returns_false()
|
|
{
|
|
var (dispatcher, kb, _, bindings) = Build();
|
|
bindings.Add(new Binding(new KeyChord(Key.A, ModifierMask.Ctrl), InputAction.SelectionExamine));
|
|
|
|
// A held without Ctrl — chord doesn't match.
|
|
kb.EmitKeyDown(Key.A, ModifierMask.None);
|
|
Assert.False(dispatcher.IsActionHeld(InputAction.SelectionExamine));
|
|
|
|
// Now release A and press Ctrl+A.
|
|
kb.EmitKeyUp(Key.A, ModifierMask.None);
|
|
kb.EmitKeyDown(Key.A, ModifierMask.Ctrl);
|
|
Assert.True(dispatcher.IsActionHeld(InputAction.SelectionExamine));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsActionHeld_any_of_multiple_bindings_satisfies()
|
|
{
|
|
var (dispatcher, kb, _, bindings) = Build();
|
|
bindings.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward));
|
|
bindings.Add(new Binding(new KeyChord(Key.Up, ModifierMask.None), InputAction.MovementForward));
|
|
|
|
kb.EmitKeyDown(Key.Up, ModifierMask.None);
|
|
Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward));
|
|
|
|
kb.EmitKeyUp(Key.Up, ModifierMask.None);
|
|
Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward));
|
|
|
|
kb.EmitKeyDown(Key.W, ModifierMask.None);
|
|
Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsActionHeld_works_for_mouse_button_chord()
|
|
{
|
|
var (dispatcher, _, mouse, bindings) = Build();
|
|
var rmb = new KeyChord(InputDispatcher.MouseButtonToKey(MouseButton.Right), ModifierMask.None, Device: 1);
|
|
bindings.Add(new Binding(rmb, InputAction.AcdreamRmbOrbitHold, ActivationType.Hold));
|
|
|
|
Assert.False(dispatcher.IsActionHeld(InputAction.AcdreamRmbOrbitHold));
|
|
|
|
mouse.EmitMouseDown(MouseButton.Right, ModifierMask.None);
|
|
Assert.True(dispatcher.IsActionHeld(InputAction.AcdreamRmbOrbitHold));
|
|
|
|
mouse.EmitMouseUp(MouseButton.Right, ModifierMask.None);
|
|
Assert.False(dispatcher.IsActionHeld(InputAction.AcdreamRmbOrbitHold));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsActionHeld_returns_false_for_None_action()
|
|
{
|
|
var (dispatcher, _, _, _) = Build();
|
|
Assert.False(dispatcher.IsActionHeld(InputAction.None));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsActionHeld_None_chord_remains_held_when_user_adds_Shift()
|
|
{
|
|
// K-fix3 (2026-04-26): Shift is the walk modifier in retail AC.
|
|
// The user holding W (default = run) and then pressing Shift
|
|
// should drop them to walk speed, NOT stop forward motion. Prior
|
|
// to this fix, IsChordHeld required CurrentModifiers to match
|
|
// chord.Modifiers EXACTLY — so (W, None) failed to match while
|
|
// CurrentModifiers=Shift, and the player stopped on Shift-press.
|
|
// Now: when chord requires no modifiers, Shift is allowed to
|
|
// coexist (other modifiers — Ctrl, Alt, Win — still mismatch).
|
|
var (dispatcher, kb, _, bindings) = Build();
|
|
bindings.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward));
|
|
|
|
kb.EmitKeyDown(Key.W, ModifierMask.None);
|
|
Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward));
|
|
|
|
// User now holds Shift while still holding W. CurrentModifiers
|
|
// becomes Shift; W is still physically down.
|
|
kb.CurrentModifiers = ModifierMask.Shift;
|
|
Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsActionHeld_None_chord_does_not_fire_when_user_adds_Ctrl()
|
|
{
|
|
// Counterpart to the Shift test above: Ctrl is NOT a movement
|
|
// modifier, so Ctrl+W should be a different chord. Without an
|
|
// explicit (W, Ctrl) binding the action stays inactive — that's
|
|
// what makes Ctrl+F* / Ctrl+1-9 / etc. distinct from the bare
|
|
// F* / 1-9 chords.
|
|
var (dispatcher, kb, _, bindings) = Build();
|
|
bindings.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward));
|
|
|
|
kb.EmitKeyDown(Key.W, ModifierMask.None);
|
|
Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward));
|
|
|
|
kb.CurrentModifiers = ModifierMask.Ctrl;
|
|
Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsActionHeld_gated_off_while_keyboard_captured()
|
|
{
|
|
// Write-mode gate (2026-06-16): a focused chat input sets
|
|
// WantCaptureKeyboard, and held-key polling then reads RELEASED so typing
|
|
// "swd" doesn't move the character. This SUPERSEDES the old design (where the
|
|
// per-frame OnUpdate guard early-returned out of the whole movement block) —
|
|
// that approach also killed AUTORUN. By gating here instead, the movement block
|
|
// keeps running, so autorun (a separate latched bool ORed into Forward at the
|
|
// call site, NOT a polled key) survives write mode. WantCaptureMouse alone does
|
|
// NOT gate held-key polling — only keyboard capture does.
|
|
var (dispatcher, kb, mouse, bindings) = Build();
|
|
bindings.Add(new Binding(new KeyChord(Key.W, ModifierMask.None), InputAction.MovementForward));
|
|
kb.EmitKeyDown(Key.W, ModifierMask.None);
|
|
|
|
// Held, no capture → reads held.
|
|
Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward));
|
|
|
|
// Keyboard captured (write mode) → held-key polling reads released.
|
|
mouse.WantCaptureKeyboard = true;
|
|
Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward));
|
|
|
|
// Mouse capture alone must NOT gate movement polling (only keyboard does).
|
|
mouse.WantCaptureKeyboard = false;
|
|
mouse.WantCaptureMouse = true;
|
|
Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward));
|
|
}
|
|
}
|