fix #414: cursor disappears at character select after the in-world logoff

Session teardown (PlayerModeController.Exit/ResetSession ->
CameraController.ExitChaseMode) fell back to the dev free-fly camera, and
CameraPointerInputController.ApplyCursorForCameraMode faithfully applies
CursorMode.Raw (GLFW disabled cursor: hidden + captured) for fly mode —
so the character-select screen after an in-world logoff had no mouse.
Fresh boot starts in Orbit and never fires a mode change, which is why
only the post-logout path was affected.

Teardown now 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).

Proven live both directions with a driven logout (UI probe 0x100000FA ->
dialog accept 0x17) under Win32 GetCursorInfo sampling: before, flags
flipped 1->0 exactly at the roster re-push that re-shows character select
and stayed hidden; after, zero hidden samples across the full timeline.
Files #415: the UI-probe 'wait world-visible' verb reads the reset
transit snapshot and is dead after reveal completion (test apparatus
only).

App tests 5564/3 skips (+3), Runtime 1756/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-17 15:17:50 +02:00
parent 70f7f72d62
commit 7aa08045d8
3 changed files with 102 additions and 5 deletions

View file

@ -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()
{