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>
198 lines
5.7 KiB
C#
198 lines
5.7 KiB
C#
using AcDream.App.Rendering;
|
|
using AcDream.Core.Rendering;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
[Collection(CameraDiagnosticsCollection.Name)]
|
|
public class CameraControllerTests
|
|
{
|
|
private static (CameraController ctl, ChaseCamera legacy, RetailChaseCamera retail) MakeChaseFixture()
|
|
{
|
|
var orbit = new OrbitCamera();
|
|
var fly = new FlyCamera();
|
|
var ctl = new CameraController(orbit, fly);
|
|
var legacy = new ChaseCamera();
|
|
var retail = new RetailChaseCamera();
|
|
ctl.EnterChaseMode(legacy, retail);
|
|
return (ctl, legacy, retail);
|
|
}
|
|
|
|
[Fact]
|
|
public void ChaseMode_WhenFlagOff_ActiveIsLegacy()
|
|
{
|
|
bool saved = CameraDiagnostics.UseRetailChaseCamera;
|
|
try
|
|
{
|
|
CameraDiagnostics.UseRetailChaseCamera = false;
|
|
var (ctl, legacy, _) = MakeChaseFixture();
|
|
Assert.Same(legacy, ctl.Active);
|
|
Assert.True(ctl.IsChaseMode);
|
|
}
|
|
finally
|
|
{
|
|
CameraDiagnostics.UseRetailChaseCamera = saved;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ChaseMode_WhenFlagOn_ActiveIsRetail()
|
|
{
|
|
bool saved = CameraDiagnostics.UseRetailChaseCamera;
|
|
try
|
|
{
|
|
CameraDiagnostics.UseRetailChaseCamera = true;
|
|
var (ctl, _, retail) = MakeChaseFixture();
|
|
Assert.Same(retail, ctl.Active);
|
|
Assert.True(ctl.IsChaseMode);
|
|
}
|
|
finally
|
|
{
|
|
CameraDiagnostics.UseRetailChaseCamera = saved;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ChaseMode_FlagFlipped_ActiveSwaps()
|
|
{
|
|
bool saved = CameraDiagnostics.UseRetailChaseCamera;
|
|
try
|
|
{
|
|
CameraDiagnostics.UseRetailChaseCamera = false;
|
|
var (ctl, legacy, retail) = MakeChaseFixture();
|
|
Assert.Same(legacy, ctl.Active);
|
|
|
|
CameraDiagnostics.UseRetailChaseCamera = true;
|
|
Assert.Same(retail, ctl.Active);
|
|
|
|
CameraDiagnostics.UseRetailChaseCamera = false;
|
|
Assert.Same(legacy, ctl.Active);
|
|
}
|
|
finally
|
|
{
|
|
CameraDiagnostics.UseRetailChaseCamera = saved;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ExitChaseMode_ClearsBothCameras()
|
|
{
|
|
bool saved = CameraDiagnostics.UseRetailChaseCamera;
|
|
try
|
|
{
|
|
CameraDiagnostics.UseRetailChaseCamera = false;
|
|
var (ctl, _, _) = MakeChaseFixture();
|
|
ctl.ExitChaseMode();
|
|
|
|
Assert.Null(ctl.Chase);
|
|
Assert.Null(ctl.RetailChase);
|
|
Assert.False(ctl.IsChaseMode);
|
|
}
|
|
finally
|
|
{
|
|
CameraDiagnostics.UseRetailChaseCamera = saved;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ExitChaseMode_AlreadyOfflinePreservesOrbitCamera()
|
|
{
|
|
var orbit = new OrbitCamera();
|
|
var ctl = new CameraController(orbit, new FlyCamera());
|
|
|
|
ctl.ExitChaseMode();
|
|
|
|
Assert.Same(orbit, ctl.Active);
|
|
Assert.False(ctl.IsFlyMode);
|
|
Assert.False(ctl.IsChaseMode);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExitChaseMode_AfterChaseToFlyReleasesRetainedSessionCameras()
|
|
{
|
|
var (ctl, _, _) = MakeChaseFixture();
|
|
ctl.ToggleFly();
|
|
Assert.True(ctl.IsFlyMode);
|
|
Assert.NotNull(ctl.Chase);
|
|
Assert.NotNull(ctl.RetailChase);
|
|
|
|
ctl.ExitChaseMode();
|
|
|
|
// 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()
|
|
{
|
|
var orbit = new OrbitCamera();
|
|
var ctl = new CameraController(orbit, new FlyCamera());
|
|
CameraController.CameraState prior = ctl.CaptureState();
|
|
int notifications = 0;
|
|
ctl.ModeChanged += _ =>
|
|
{
|
|
if (++notifications == 1)
|
|
throw new InvalidOperationException("injected camera observer failure");
|
|
};
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
ctl.EnterChaseMode(new ChaseCamera(), new RetailChaseCamera()));
|
|
|
|
ctl.RestoreState(prior);
|
|
|
|
Assert.Same(orbit, ctl.Active);
|
|
Assert.False(ctl.IsFlyMode);
|
|
Assert.False(ctl.IsChaseMode);
|
|
Assert.Null(ctl.Chase);
|
|
Assert.Null(ctl.RetailChase);
|
|
Assert.Equal(2, notifications);
|
|
}
|
|
}
|