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;
}