Move local teleport, player-mode, animation, shadow, and sealed-dungeon lifetimes out of GameWindow. Make player-mode entry transactional and isolate approach completions by controller lifetime and approach generation so stale callbacks cannot affect replacements. Co-authored-by: Codex <noreply@openai.com>
107 lines
3.2 KiB
C#
107 lines
3.2 KiB
C#
// src/AcDream.App/Rendering/CameraController.cs
|
|
using AcDream.Core.Rendering;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
public sealed class CameraController
|
|
{
|
|
internal readonly record struct CameraState(
|
|
int ModeCode,
|
|
ChaseCamera? Chase,
|
|
RetailChaseCamera? RetailChase);
|
|
|
|
public OrbitCamera Orbit { get; }
|
|
public FlyCamera Fly { get; }
|
|
public ChaseCamera? Chase { get; private set; }
|
|
public RetailChaseCamera? RetailChase { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The renderer-facing active camera. Both the legacy and retail
|
|
/// chase cameras are held simultaneously so that flipping
|
|
/// <see cref="CameraDiagnostics.UseRetailChaseCamera"/> takes effect
|
|
/// on the very next access to this property — no re-entry required,
|
|
/// no notification mechanism, no stale state.
|
|
/// </summary>
|
|
public ICamera Active
|
|
{
|
|
get
|
|
{
|
|
if (_mode == Mode.Fly) return Fly;
|
|
if (_mode == Mode.Chase)
|
|
{
|
|
if (CameraDiagnostics.UseRetailChaseCamera && RetailChase is not null)
|
|
return RetailChase;
|
|
if (Chase is not null) return Chase;
|
|
}
|
|
return Orbit;
|
|
}
|
|
}
|
|
|
|
public bool IsFlyMode => _mode == Mode.Fly;
|
|
public bool IsChaseMode => _mode == Mode.Chase;
|
|
|
|
public event Action<bool>? ModeChanged;
|
|
|
|
private enum Mode { Orbit, Fly, Chase }
|
|
private Mode _mode = Mode.Orbit;
|
|
|
|
public CameraController(OrbitCamera orbit, FlyCamera fly)
|
|
{
|
|
Orbit = orbit;
|
|
Fly = fly;
|
|
}
|
|
|
|
public void ToggleFly()
|
|
{
|
|
_mode = IsFlyMode ? Mode.Orbit : Mode.Fly;
|
|
ModeChanged?.Invoke(IsFlyMode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Store both cameras simultaneously; <see cref="Active"/> picks
|
|
/// between them per-read via the flag — no re-entry needed on flip.
|
|
/// </summary>
|
|
public void EnterChaseMode(ChaseCamera legacy, RetailChaseCamera retail)
|
|
{
|
|
Chase = legacy;
|
|
RetailChase = retail;
|
|
_mode = Mode.Chase;
|
|
ModeChanged?.Invoke(IsChaseMode);
|
|
}
|
|
|
|
public void ExitChaseMode()
|
|
{
|
|
bool wasChaseMode = IsChaseMode;
|
|
Chase = null;
|
|
RetailChase = null;
|
|
if (_mode == Mode.Orbit)
|
|
return;
|
|
_mode = Mode.Fly;
|
|
if (wasChaseMode)
|
|
ModeChanged?.Invoke(IsFlyMode);
|
|
}
|
|
|
|
public void SetAspect(float aspect)
|
|
{
|
|
Orbit.Aspect = aspect;
|
|
Fly.Aspect = aspect;
|
|
}
|
|
|
|
internal CameraState CaptureState() =>
|
|
new((int)_mode, Chase, RetailChase);
|
|
|
|
/// <summary>
|
|
/// Restores a previously captured camera lifetime before notifying
|
|
/// subscribers. State remains restored even when a subscriber throws.
|
|
/// </summary>
|
|
internal void RestoreState(CameraState state)
|
|
{
|
|
if (state.ModeCode < (int)Mode.Orbit || state.ModeCode > (int)Mode.Chase)
|
|
throw new ArgumentOutOfRangeException(nameof(state));
|
|
|
|
Chase = state.Chase;
|
|
RetailChase = state.RetailChase;
|
|
_mode = (Mode)state.ModeCode;
|
|
ModeChanged?.Invoke(IsFlyMode || IsChaseMode);
|
|
}
|
|
}
|