feat(D.2b): write-mode movement gate that preserves autorun

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>
This commit is contained in:
Erik 2026-06-16 15:24:19 +02:00
parent 367a752078
commit 2284a376ae
3 changed files with 37 additions and 17 deletions

View file

@ -1854,9 +1854,11 @@ public sealed class GameWindow : IDisposable
vitalsDatFont, _debugFont, ResolveChrome);
if (chatController is not null)
{
// Ctrl+C / Ctrl+A on the transcript need the keyboard for clipboard + modifiers.
// _uiHost.Keyboard is set by WireKeyboard above — it is non-null here.
// Ctrl+C / Ctrl+A on the transcript + Ctrl+C/X/V/A on the input need the
// keyboard for clipboard + modifier (Ctrl/Shift) state. _uiHost.Keyboard
// is set by WireKeyboard above — it is non-null here.
chatController.Transcript.Keyboard = _uiHost.Keyboard;
chatController.Input.Keyboard = _uiHost.Keyboard;
// Wrap the dat content in the universal 8-piece beveled window chrome —
// the SAME UiNineSlicePanel the vitals window uses. The chat's own dat
// layout only carries flat background sprites, so without this the window
@ -1887,6 +1889,10 @@ public sealed class GameWindow : IDisposable
chatRoot.Draggable = false; chatRoot.Resizable = false;
chatFrame.AddChild(chatRoot);
_uiHost.Root.AddChild(chatFrame);
// Tab / Enter enters "write mode" by focusing this input (retail's chat
// activation); a focused input suppresses character movement (see the
// WantsKeyboard gate in the movement poll).
_uiHost.Root.DefaultTextInput = chatController.Input;
Console.WriteLine("[D.2b] retail chat window from LayoutDesc importer (0x21000006).");
}
else Console.WriteLine("[D.2b] chat: required role elements missing in 0x21000006.");
@ -7271,6 +7277,11 @@ public sealed class GameWindow : IDisposable
// this guard adds defense-in-depth for the per-frame IsActionHeld
// movement poll below (typing "walk" into a chat field shouldn't
// walk).
// ImGui dev-tools text fields fully pause game input (incl. autorun) — fine, it's a
// debug overlay. The RETAIL chat "write mode" does NOT early-return here: the block
// below still runs so AUTORUN keeps driving the character while you type. Held WASD
// is silenced at the source instead — InputDispatcher.IsActionHeld returns false
// while WantCaptureKeyboard (which includes a focused chat input) is set.
bool suppressGameInput =
DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard;
if (suppressGameInput) return;

View file

@ -141,6 +141,12 @@ public sealed class InputDispatcher
public bool IsActionHeld(InputAction action)
{
if (action == InputAction.None) return false;
// While a text field owns the keyboard ("write mode"), held game actions read as
// released: typing "swd" must not move the character. This is the polling-path twin
// of the WantCaptureKeyboard gate on Fired actions. NOTE: this suppresses KEY-driven
// movement only — latched state that isn't a key (e.g. autorun, ORed into Forward at
// the call site) keeps driving the character, so chat doesn't cancel autorun.
if (_mouse.WantCaptureKeyboard) return false;
foreach (var b in _bindings.ForAction(action))
{
if (IsChordHeld(b.Chord)) return true;