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