diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 543ed119..22b0e4bb 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -24,6 +24,48 @@ What does NOT go here: - Every session: scan OPEN issues at start; promote/close anything we touched during the session before ending. - Promoting to a Phase: mark as `DONE (promoted to Phase X)` + commit SHA where the Phase entry landed. +## #415 — UI-probe `wait world-visible` verb is dead after reveal completion (automation bridge reads the reset snapshot) + +**Status:** OPEN (filed 2026-08-17 at the #414 cursor repro). The script +runner's `wait world-visible` polls +`WorldLifecycleAutomationController.IsWorldViewportVisible`, which reads +`_getReveal().WorldViewportObserved` from the LIVE transit snapshot. The +reveal's `event=complete` retires that generation and the snapshot resets +`WorldViewportObserved: false` (`RuntimeWorldTransitState.cs:583`), so the +verb only observes true during the sub-second window between +`event=world-visible` and `event=complete` — in practice it times out even +though the world revealed (proved by the #414 repro logs: `wait +world-visible 60000` timed out with `event=world-visible` present in the +same run's `[world-reveal]` stream). Fix shape: the automation controller +should latch world-visible per generation (or the verb should accept +`IsWorldReady`-style completed state), not read the transient snapshot. +Test apparatus only — no player impact. + +## #414 — Mouse cursor disappears at character select after the in-world logoff (teardown fly-mode fallback raw-captures the cursor) + +**Status:** ✅ FIXED 2026-08-17 (entry/exit presentation follow-up; fix + +regression tests in the same commit as this entry). **Symptom:** press the +indicator bar's X, confirm Yes, land on character select — the OS cursor is +gone (and captured). Second Enter still works; the cursor returns once a +world login re-enters chase mode. **Evidence:** live Win32 `GetCursorInfo` +sampling over the driven logout (scripted UI probe: `0x100000FA` → dialog +accept `0x17`) — cursor flags flipped `1 → 0` (hCursor `0x0`) exactly at +the roster re-push that re-shows character select, and stayed hidden. +**Root cause:** `CameraController.ExitChaseMode` (called from +`PlayerModeController.Exit`/`ResetSession` at every session teardown) fell +back to `Mode.Fly` — the pre-retail dev free-camera convention — and +`CameraPointerInputController.ApplyCursorForCameraMode` faithfully applies +`CursorMode.Raw` (GLFW disabled cursor: hidden + captured) for fly mode. +Fresh boot never fires a mode change at character select (starts Orbit), so +only the post-logout path was affected. **Fix:** teardown lands on +`Mode.Orbit` — the exact state a fresh boot presents at character select — +and always notifies, so the pointer controller restores `CursorMode.Normal` +even when torn down from the dev fly camera. The dev fly↔chase flow is +untouched (it rides `ToggleFly`, never `ExitChaseMode`). +`CameraControllerTests`: chase→orbit + notify, fly→orbit + notify, +orbit no-op no-notify; the old fly-fallback assertion updated to the new +contract. + ## #413 — House tab shows no content (owned-house display, Display* line builders unported) **Status:** NARROWED 2026-08-17 (House-tab ownership-text closer session); diff --git a/src/AcDream.App/Rendering/CameraController.cs b/src/AcDream.App/Rendering/CameraController.cs index a0de494f..f38762ad 100644 --- a/src/AcDream.App/Rendering/CameraController.cs +++ b/src/AcDream.App/Rendering/CameraController.cs @@ -87,16 +87,24 @@ public sealed class CameraController ModeChanged?.Invoke(IsChaseMode); } + /// + /// Player-mode teardown (logout, session reset). Lands on the neutral + /// orbit camera — the same state a fresh boot presents at character + /// select — NOT the dev free-fly camera: fly mode raw-captures the OS + /// cursor (CameraPointerInputController.ApplyCursorForCameraMode + /// sets CursorMode.Raw), and the old Mode.Fly fallback + /// left the character-select screen after an in-world logoff with a + /// hidden, captured mouse. The dev fly↔chase flow is unaffected — it + /// rides , never this teardown path. + /// public void ExitChaseMode() { - bool wasChaseMode = IsChaseMode; Chase = null; RetailChase = null; if (_mode == Mode.Orbit) return; - _mode = Mode.Fly; - if (wasChaseMode) - ModeChanged?.Invoke(IsFlyMode); + _mode = Mode.Orbit; + ModeChanged?.Invoke(false); } public void SetAspect(float aspect) diff --git a/tests/AcDream.App.Tests/Rendering/CameraControllerTests.cs b/tests/AcDream.App.Tests/Rendering/CameraControllerTests.cs index b8810c66..831690c6 100644 --- a/tests/AcDream.App.Tests/Rendering/CameraControllerTests.cs +++ b/tests/AcDream.App.Tests/Rendering/CameraControllerTests.cs @@ -118,11 +118,58 @@ public class CameraControllerTests ctl.ExitChaseMode(); - Assert.True(ctl.IsFlyMode); + // Teardown lands on orbit even from the dev fly camera: fly mode + // raw-captures the OS cursor, and a session teardown must leave the + // character-select screen with a normal visible pointer. + Assert.False(ctl.IsFlyMode); + Assert.False(ctl.IsChaseMode); Assert.Null(ctl.Chase); Assert.Null(ctl.RetailChase); } + [Fact] + public void ExitChaseMode_FromChaseLandsOnOrbitAndNotifies() + { + var (ctl, _, _) = MakeChaseFixture(); + int notifications = 0; + bool? lastArg = null; + ctl.ModeChanged += arg => { notifications++; lastArg = arg; }; + + ctl.ExitChaseMode(); + + Assert.False(ctl.IsChaseMode); + Assert.False(ctl.IsFlyMode); + Assert.Equal(1, notifications); + Assert.False(lastArg); + } + + [Fact] + public void ExitChaseMode_FromFlyNotifiesSoCursorCaptureReleases() + { + var ctl = new CameraController(new OrbitCamera(), new FlyCamera()); + ctl.ToggleFly(); + Assert.True(ctl.IsFlyMode); + int notifications = 0; + ctl.ModeChanged += _ => notifications++; + + ctl.ExitChaseMode(); + + Assert.False(ctl.IsFlyMode); + Assert.Equal(1, notifications); + } + + [Fact] + public void ExitChaseMode_FromOrbitDoesNotNotify() + { + var ctl = new CameraController(new OrbitCamera(), new FlyCamera()); + int notifications = 0; + ctl.ModeChanged += _ => notifications++; + + ctl.ExitChaseMode(); + + Assert.Equal(0, notifications); + } + [Fact] public void RestoreState_ReestablishesPriorCameraAfterNotificationFailure() {