From bc9ee9fdfa34be3539b8ac407b747604da83ea6a Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 26 Apr 2026 10:11:01 +0200 Subject: [PATCH] =?UTF-8?q?fix(input):=20Phase=20K=20live-test=20fixes=20?= =?UTF-8?q?=E2=80=94=20default-run,=20Q-autorun=20toggle,=20free=20cursor,?= =?UTF-8?q?=20no=20Holtburg=20flash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitignore | 3 + src/AcDream.App/Rendering/GameWindow.cs | 133 ++++++++++++++++-- .../Input/KeyBindings.cs | 21 ++- 3 files changed, 141 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 88e3571..a4efa6a 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ refs/ # Python tooling (under tools/) — bytecode caches __pycache__/ *.pyc + +# Per-session scratch (Claude commit message drafts, ad-hoc temp files) +tmp/ diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 6a9dc26..56f8b74 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -403,6 +403,13 @@ public sealed class GameWindow : IDisposable // the orbited position (no snap back). 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 // by the EnterWorld branch in BeginLiveSessionAsync; ticked from // OnUpdate; disarmed if the user manually enters fly mode (or any @@ -459,6 +466,34 @@ public sealed class GameWindow : IDisposable private int _liveCenterY; 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"; + + /// + /// 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 . + /// 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). + /// + 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; + /// /// Phase 6.6/6.7: server-guid → local WorldEntity lookup so /// 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.AdjustPitch(dy * 0.003f * sens); } - else - { - // Without RMB or MMB held, mouse only pitches the - // chase camera (Y-axis). Mouse X is dropped — - // character turning is keyboard-only. - _chaseCamera.AdjustPitch(dy * 0.003f * sens); - } + // K-fix1 (2026-04-26): no default-pitch path. With + // neither MMB nor RMB held, mouse moves the cursor + // freely so the user can interact with panels + + // (eventually) click selectables in the world. Pitch + // is gated on a deliberate hold input. } else if (_cameraController.IsFlyMode) { @@ -3661,7 +3694,14 @@ public sealed class GameWindow : IDisposable // login to land before any landblock was loaded; AppendLiveEntity // is a no-op for unloaded landblocks, so all 40+ NPCs/weenies were // 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 observerCy = _liveCenterY; @@ -3789,15 +3829,27 @@ public sealed class GameWindow : IDisposable if (_inputDispatcher is null) return; _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( - Forward: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementForward), + Forward: wHeld || _autoRunActive, Backward: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementBackup), StrafeLeft: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementStrafeLeft), StrafeRight: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementStrafeRight), TurnLeft: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementTurnLeft), TurnRight: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementTurnRight), - Run: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementRunLock), - MouseDeltaX: 0f, // K.1b: mouse never drives character yaw + Run: !walking, // default = run; Shift held = walk + MouseDeltaX: 0f, Jump: _inputDispatcher.IsActionHeld(AcDream.UI.Abstractions.Input.InputAction.MovementJump)); var result = _playerController.Update((float)dt, input); @@ -3941,9 +3993,13 @@ public sealed class GameWindow : IDisposable var mouse = _input.Mice.FirstOrDefault(); if (mouse is null) return; - // Raw cursor mode for both fly AND chase (player) mode — both need - // mouse deltas for look/turn. Only orbit mode uses normal cursor. - bool needsRawCursor = isFlyMode || _playerMode; + // K-fix1 (2026-04-26): cursor visible by default in chase / orbit + // modes — the user needs to click panels, dropdowns, the future + // 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; _capturedMouse = needsRawCursor ? mouse : null; } @@ -4074,6 +4130,18 @@ public sealed class GameWindow : IDisposable _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); // Conditional depth clear: when camera is inside a building, clear @@ -4214,6 +4282,14 @@ public sealed class GameWindow : IDisposable _lastNearestObjDist = bestDist < 0f ? 0f : bestDist; _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 @@ -5193,6 +5269,29 @@ public sealed class GameWindow : IDisposable // re-fire. 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) { 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. _playerMouseDeltaX = 0f; _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; } diff --git a/src/AcDream.UI.Abstractions/Input/KeyBindings.cs b/src/AcDream.UI.Abstractions/Input/KeyBindings.cs index 58ba368..d2020b0 100644 --- a/src/AcDream.UI.Abstractions/Input/KeyBindings.cs +++ b/src/AcDream.UI.Abstractions/Input/KeyBindings.cs @@ -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.Right, ModifierMask.Alt), InputAction.MovementStrafeRight)); // Walk-mode modifier — Hold so a subscriber can latch state on - // press and unlatch on release. - b.Add(new(new KeyChord(Key.ShiftLeft, ModifierMask.None), InputAction.MovementWalkMode, ActivationType.Hold)); + // press and unlatch on release. K-fix1 (2026-04-26): the chord + // 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.S, ModifierMask.None), InputAction.MovementStop)); 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.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; }