fix(input): Phase K live-test fixes — default-run, Q-autorun toggle, free cursor, no Holtburg flash

Four issues from K.3 live verification (2026-04-26 user report):

1. Default movement speed should be RUN, not walk.
   PlayerMovementController.MovementInput.Run was sourced from
   IsActionHeld(MovementRunLock) (Q held). Inverted to
   !IsActionHeld(MovementWalkMode) (Shift held = walk; default = run).

   Also fixed RetailDefaults() — MovementWalkMode was bound to
   (ShiftLeft, ModifierMask.None), but when LShift IS the primary
   key the OS keyboard reports CurrentModifiers=Shift and the
   chord lookup mismatches. Bind both LShift+Shift and RShift+Shift
   to match (the same fix AcdreamCurrentDefaults already had).

2. Q is autorun TOGGLE, not hold-to-run. Added _autoRunActive
   field; OnInputAction toggles it on MovementRunLock Press;
   MovementInput.Forward now ORs in _autoRunActive so autorun
   stays latched until canceled. Pressing Backup / Stop /
   StrafeLeft / StrafeRight clears the latch (deliberate movement
   wins, retail-faithful). Pressing Forward AGAIN does NOT cancel —
   matches retail's stack semantics.

3. Mouse cursor visible by default in chase mode + no Y-axis
   steering without an explicit hold input. OnCameraModeChanged
   now uses CursorMode.Normal for chase (was Raw — invisible
   pointer). MouseMove handler's "neither RMB nor MMB held"
   branch dropped its AdjustPitch call — pitch is gated to
   deliberate hold inputs only. Fly mode keeps Raw (continuous
   look-and-fly affordance).

   Restored AcdreamRmbOrbitHold binding in RetailDefaults() —
   K.1c silently dropped it when SelectRight took the RMB Press
   slot; the Hold-type binding coexists with Press so RMB orbit
   still works in addition to (future) SelectRight click.

4. Holtburg flashes briefly at live login. Added
   IsLiveModeWaitingForLogin gate (true iff ACDREAM_LIVE=1 AND
   chase camera has not yet been entered) that:
     * suppresses StreamingController.Tick in OnUpdate so no
       landblocks load around the hardcoded startup center
       0xA9B4 (Holtburg);
     * skips terrain + entity rendering in OnRender via a
       SkipWorldGeometry label after the sky pass.
   Sky still draws so the user sees a live, time-of-day-correct
   sky during the connection / character-list / EnterWorld
   handshake. Latches off once chase mode has been entered, so
   later fly-mode toggles render the world normally.

Tests still 1220 green.

Also commits .gitignore tmp/ rule (left over from K.3
session) — gitignored per-session scratch (commit message
drafts, ad-hoc temp files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-26 10:11:01 +02:00
parent f42c164b90
commit bc9ee9fdfa
3 changed files with 141 additions and 16 deletions

3
.gitignore vendored
View file

@ -35,3 +35,6 @@ refs/
# Python tooling (under tools/) — bytecode caches # Python tooling (under tools/) — bytecode caches
__pycache__/ __pycache__/
*.pyc *.pyc
# Per-session scratch (Claude commit message drafts, ad-hoc temp files)
tmp/

View file

@ -403,6 +403,13 @@ public sealed class GameWindow : IDisposable
// the orbited position (no snap back). // the orbited position (no snap back).
private bool _rmbHeld; private bool _rmbHeld;
// K-fix1 (2026-04-26): autorun is a TOGGLE — Press Q to start
// forward-running, press Q again (or any movement-cancel key like
// X / S / Backward / Forward) to stop. Mirrors retail's
// AutoRun action. While true, MovementInput.Forward is forced
// true regardless of W's state.
private bool _autoRunActive;
// Phase K.2 — auto-enter player mode after a successful login. Armed // Phase K.2 — auto-enter player mode after a successful login. Armed
// by the EnterWorld branch in BeginLiveSessionAsync; ticked from // by the EnterWorld branch in BeginLiveSessionAsync; ticked from
// OnUpdate; disarmed if the user manually enters fly mode (or any // OnUpdate; disarmed if the user manually enters fly mode (or any
@ -459,6 +466,34 @@ public sealed class GameWindow : IDisposable
private int _liveCenterY; private int _liveCenterY;
private uint _liveEntityIdCounter = 1_000_000u; // well above any dat-hydrated id private uint _liveEntityIdCounter = 1_000_000u; // well above any dat-hydrated id
// K-fix1 (2026-04-26): cached at startup so per-frame branches are
// single-flag reads instead of env-var lookups. True iff
// ACDREAM_LIVE=1 was set when the window came up.
private static readonly bool LiveModeEnabled =
Environment.GetEnvironmentVariable("ACDREAM_LIVE") == "1";
/// <summary>
/// K-fix1 (2026-04-26): true iff live mode is configured AND we have
/// NOT yet handed control to the chase camera. Gates initial
/// streaming + scene rendering so the user never sees Holtburg flash
/// before login completes — the screen stays at the sky/fog clear
/// color until the player entity has spawned + the auto-entry guard
/// has triggered <see cref="EnterPlayerModeFromAutoEntry"/>.
/// Offline (LiveModeEnabled false) returns false → unchanged
/// orbit-camera Holtburg view stays the foreground. Once chase mode
/// is active the property latches false for the rest of the
/// session, even if the user later toggles into fly mode (we don't
/// want to re-blank the world after they've seen it).
/// </summary>
private bool IsLiveModeWaitingForLogin =>
LiveModeEnabled
&& !_chaseModeEverEntered;
// Latches true the first time chase mode becomes active. Used by
// IsLiveModeWaitingForLogin to suppress the pre-login render gate
// for subsequent fly-mode toggles.
private bool _chaseModeEverEntered;
/// <summary> /// <summary>
/// Phase 6.6/6.7: server-guid → local WorldEntity lookup so /// Phase 6.6/6.7: server-guid → local WorldEntity lookup so
/// UpdateMotion and UpdatePosition handlers can find the entity the /// UpdateMotion and UpdatePosition handlers can find the entity the
@ -646,13 +681,11 @@ public sealed class GameWindow : IDisposable
_chaseCamera.YawOffset -= dx * 0.004f * sens; _chaseCamera.YawOffset -= dx * 0.004f * sens;
_chaseCamera.AdjustPitch(dy * 0.003f * sens); _chaseCamera.AdjustPitch(dy * 0.003f * sens);
} }
else // K-fix1 (2026-04-26): no default-pitch path. With
{ // neither MMB nor RMB held, mouse moves the cursor
// Without RMB or MMB held, mouse only pitches the // freely so the user can interact with panels +
// chase camera (Y-axis). Mouse X is dropped — // (eventually) click selectables in the world. Pitch
// character turning is keyboard-only. // is gated on a deliberate hold input.
_chaseCamera.AdjustPitch(dy * 0.003f * sens);
}
} }
else if (_cameraController.IsFlyMode) else if (_cameraController.IsFlyMode)
{ {
@ -3661,7 +3694,14 @@ public sealed class GameWindow : IDisposable
// login to land before any landblock was loaded; AppendLiveEntity // login to land before any landblock was loaded; AppendLiveEntity
// is a no-op for unloaded landblocks, so all 40+ NPCs/weenies were // is a no-op for unloaded landblocks, so all 40+ NPCs/weenies were
// silently dropped on the first frame and never rendered. // silently dropped on the first frame and never rendered.
if (_streamingController is not null && _cameraController is not null) //
// K-fix1 (2026-04-26): skip streaming entirely while live mode is
// configured but the chase camera hasn't engaged yet — otherwise
// the orbit camera at startup centers on the hardcoded
// 0xA9B4 (Holtburg) and Holtburg landblocks render briefly
// until the player spawn arrives + auto-entry switches to chase.
if (_streamingController is not null && _cameraController is not null
&& !IsLiveModeWaitingForLogin)
{ {
int observerCx = _liveCenterX; int observerCx = _liveCenterX;
int observerCy = _liveCenterY; int observerCy = _liveCenterY;
@ -3789,15 +3829,27 @@ public sealed class GameWindow : IDisposable
if (_inputDispatcher is null) return; if (_inputDispatcher is null) return;
_playerMouseDeltaX = 0f; // defensive: ensure no leakage even if some path writes it _playerMouseDeltaX = 0f; // defensive: ensure no leakage even if some path writes it
// K-fix1 (2026-04-26): retail-faithful movement semantics.
// * Default speed = RUN. Forward / backward / strafe all run
// by default; holding Shift (MovementWalkMode) drops to
// walk speed.
// * Q = AUTORUN TOGGLE: pressing Q latches forward-running
// until Q is pressed again. Handled in OnInputAction; here
// we just OR _autoRunActive into the Forward flag.
// * Mouse never drives character yaw (K.1b regression-prevention).
bool walking = _inputDispatcher.IsActionHeld(
AcDream.UI.Abstractions.Input.InputAction.MovementWalkMode);
bool wHeld = _inputDispatcher.IsActionHeld(
AcDream.UI.Abstractions.Input.InputAction.MovementForward);
var input = new AcDream.App.Input.MovementInput( var input = new AcDream.App.Input.MovementInput(
Forward: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementForward), Forward: wHeld || _autoRunActive,
Backward: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementBackup), Backward: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementBackup),
StrafeLeft: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementStrafeLeft), StrafeLeft: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementStrafeLeft),
StrafeRight: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementStrafeRight), StrafeRight: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementStrafeRight),
TurnLeft: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementTurnLeft), TurnLeft: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementTurnLeft),
TurnRight: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementTurnRight), TurnRight: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementTurnRight),
Run: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementRunLock), Run: !walking, // default = run; Shift held = walk
MouseDeltaX: 0f, // K.1b: mouse never drives character yaw MouseDeltaX: 0f,
Jump: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementJump)); Jump: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementJump));
var result = _playerController.Update((float)dt, input); var result = _playerController.Update((float)dt, input);
@ -3941,9 +3993,13 @@ public sealed class GameWindow : IDisposable
var mouse = _input.Mice.FirstOrDefault(); var mouse = _input.Mice.FirstOrDefault();
if (mouse is null) return; if (mouse is null) return;
// Raw cursor mode for both fly AND chase (player) mode — both need // K-fix1 (2026-04-26): cursor visible by default in chase / orbit
// mouse deltas for look/turn. Only orbit mode uses normal cursor. // modes — the user needs to click panels, dropdowns, the future
bool needsRawCursor = isFlyMode || _playerMode; // selection picker, etc. Mouse-look (raw mode) only happens
// transiently while MMB is held (HideCursorForMouseLook /
// RestoreCursorAfterMouseLook). Fly mode still needs raw because
// it's a continuous look-and-fly affordance.
bool needsRawCursor = isFlyMode;
mouse.Cursor.CursorMode = needsRawCursor ? CursorMode.Raw : CursorMode.Normal; mouse.Cursor.CursorMode = needsRawCursor ? CursorMode.Raw : CursorMode.Normal;
_capturedMouse = needsRawCursor ? mouse : null; _capturedMouse = needsRawCursor ? mouse : null;
} }
@ -4074,6 +4130,18 @@ public sealed class GameWindow : IDisposable
_activeDayGroup, kf); _activeDayGroup, kf);
} }
// K-fix1 (2026-04-26): suppress terrain + entity rendering
// while live mode is configured but the chase camera hasn't
// engaged yet — pairs with the streaming-Tick gate in
// OnUpdate so absolutely nothing of the world (Holtburg or
// otherwise) renders pre-login. The sky still draws above so
// the user sees a live, time-of-day-correct sky during the
// brief connection + character-list + EnterWorld handshake.
if (IsLiveModeWaitingForLogin)
{
goto SkipWorldGeometry;
}
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb); _terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
// Conditional depth clear: when camera is inside a building, clear // Conditional depth clear: when camera is inside a building, clear
@ -4214,6 +4282,14 @@ public sealed class GameWindow : IDisposable
_lastNearestObjDist = bestDist < 0f ? 0f : bestDist; _lastNearestObjDist = bestDist < 0f ? 0f : bestDist;
_lastNearestObjLabel = bestLabel; _lastNearestObjLabel = bestLabel;
} }
// K-fix1 (2026-04-26): jump target for IsLiveModeWaitingForLogin —
// skips the world geometry pass before login. ImGui (chat,
// debug, settings panels) and the menu bar still render
// below. Sky has already drawn before this label so the
// pre-login screen shows a live, correctly-tinted sky and
// nothing else.
SkipWorldGeometry: ;
} }
// Phase D.2a — end ImGui frame. Runs AFTER all scene + debug draws // Phase D.2a — end ImGui frame. Runs AFTER all scene + debug draws
@ -5193,6 +5269,29 @@ public sealed class GameWindow : IDisposable
// re-fire. // re-fire.
if (activation != AcDream.UI.Abstractions.Input.ActivationType.Press) return; if (activation != AcDream.UI.Abstractions.Input.ActivationType.Press) return;
// K-fix1 (2026-04-26): Q is autorun TOGGLE, not hold-to-run. Press
// Q to start, press Q again to stop. Pressing Backup / Stop /
// StrafeLeft / StrafeRight while autorun is active also cancels it
// — retail-faithful: any deliberate movement input wins. (Pressing
// Forward AGAIN does NOT cancel — retail's autorun stays active
// even when you press W; the two stack.)
if (action == AcDream.UI.Abstractions.Input.InputAction.MovementRunLock)
{
_autoRunActive = !_autoRunActive;
return;
}
if (_autoRunActive
&& (action == AcDream.UI.Abstractions.Input.InputAction.MovementBackup
|| action == AcDream.UI.Abstractions.Input.InputAction.MovementStop
|| action == AcDream.UI.Abstractions.Input.InputAction.MovementStrafeLeft
|| action == AcDream.UI.Abstractions.Input.InputAction.MovementStrafeRight))
{
_autoRunActive = false;
// Fall through — these actions still need their normal handling
// (e.g. Stop is currently a no-op in the switch, but keeping the
// fall-through means future logic fires).
}
switch (action) switch (action)
{ {
case AcDream.UI.Abstractions.Input.InputAction.AcdreamToggleDebugPanel: case AcDream.UI.Abstractions.Input.InputAction.AcdreamToggleDebugPanel:
@ -5391,6 +5490,12 @@ public sealed class GameWindow : IDisposable
// into a future code path that re-enables mouse-yaw. // into a future code path that re-enables mouse-yaw.
_playerMouseDeltaX = 0f; _playerMouseDeltaX = 0f;
_cameraController?.EnterChaseMode(_chaseCamera); _cameraController?.EnterChaseMode(_chaseCamera);
// K-fix1 (2026-04-26): latch the "we have entered chase at least
// once" flag so the live-mode pre-login render gate stops
// suppressing the scene. From here on, the orbit camera (if the
// user ever returns to it via Escape) shows whatever's loaded —
// we don't re-blank the world.
_chaseModeEverEntered = true;
return true; return true;
} }

View file

@ -152,8 +152,14 @@ public sealed class KeyBindings
b.Add(new(new KeyChord(Key.D, ModifierMask.Alt), InputAction.MovementStrafeRight)); b.Add(new(new KeyChord(Key.D, ModifierMask.Alt), InputAction.MovementStrafeRight));
b.Add(new(new KeyChord(Key.Right, ModifierMask.Alt), InputAction.MovementStrafeRight)); b.Add(new(new KeyChord(Key.Right, ModifierMask.Alt), InputAction.MovementStrafeRight));
// Walk-mode modifier — Hold so a subscriber can latch state on // Walk-mode modifier — Hold so a subscriber can latch state on
// press and unlatch on release. // press and unlatch on release. K-fix1 (2026-04-26): the chord
b.Add(new(new KeyChord(Key.ShiftLeft, ModifierMask.None), InputAction.MovementWalkMode, ActivationType.Hold)); // modifier MUST be Shift, not None — when LShift/RShift is the
// primary key the OS keyboard reports CurrentModifiers=Shift
// alongside the key-down. Bind both left + right shift to match.
// This is the same pattern AcdreamCurrentDefaults uses for its
// Shift→RunLock binding (see lines 98-99 above).
b.Add(new(new KeyChord(Key.ShiftLeft, ModifierMask.Shift), InputAction.MovementWalkMode, ActivationType.Hold));
b.Add(new(new KeyChord(Key.ShiftRight, ModifierMask.Shift), InputAction.MovementWalkMode, ActivationType.Hold));
b.Add(new(new KeyChord(Key.Q, ModifierMask.None), InputAction.MovementRunLock)); b.Add(new(new KeyChord(Key.Q, ModifierMask.None), InputAction.MovementRunLock));
b.Add(new(new KeyChord(Key.S, ModifierMask.None), InputAction.MovementStop)); b.Add(new(new KeyChord(Key.S, ModifierMask.None), InputAction.MovementStop));
b.Add(new(new KeyChord(Key.Y, ModifierMask.None), InputAction.Ready)); b.Add(new(new KeyChord(Key.Y, ModifierMask.None), InputAction.Ready));
@ -333,6 +339,17 @@ public sealed class KeyBindings
b.Add(new(new KeyChord(Key.F9, ModifierMask.Ctrl), InputAction.AcdreamSensitivityUp)); b.Add(new(new KeyChord(Key.F9, ModifierMask.Ctrl), InputAction.AcdreamSensitivityUp));
b.Add(new(new KeyChord(Key.F10, ModifierMask.Ctrl), InputAction.AcdreamCycleWeather)); b.Add(new(new KeyChord(Key.F10, ModifierMask.Ctrl), InputAction.AcdreamCycleWeather));
// K-fix1 (2026-04-26): RMB-hold camera orbit. Coexists with the
// SelectRight Press binding above — Press fires on click,
// AcdreamRmbOrbitHold fires on hold/release transitions so the
// chase camera can free-orbit while the user drags the mouse.
// Without this, RMB-orbit silently broke when K.1c flipped the
// default keymap from AcdreamCurrentDefaults to RetailDefaults.
b.Add(new(
new KeyChord(InputDispatcher.MouseButtonToKey(MouseButton.Right), ModifierMask.None, Device: 1),
InputAction.AcdreamRmbOrbitHold,
ActivationType.Hold));
return b; return b;
} }