refactor(update): cut over the frame orchestrator

Reduce GameWindow.OnUpdate to one typed orchestration call and remove transitive window callbacks from teardown, liveness, teleport placement, live presentation, auto-entry, and entity packet routing. Preserve the frozen retail object/network order while making the production owner graph explicit and testable.
This commit is contained in:
Erik 2026-07-22 03:31:38 +02:00
parent 947c61d2d7
commit e91f310279
18 changed files with 864 additions and 348 deletions

View file

@ -1,11 +1,14 @@
using System;
using AcDream.App.Net;
using AcDream.App.Streaming;
using AcDream.App.World;
namespace AcDream.App.Input;
/// <summary>
/// Phase K.2 — one-shot guard that auto-enters player mode after a
/// successful login once every prerequisite is satisfied. Owned by
/// <c>GameWindow</c> and ticked each frame from <c>OnUpdate</c>.
/// successful login once every prerequisite is satisfied. The update-frame
/// orchestrator ticks it through a typed production context.
///
/// <para>
/// Why is this its own class? The auto-entry has four independent
@ -33,20 +36,108 @@ namespace AcDream.App.Input;
/// </para>
///
/// <para>
/// All preconditions are passed in as predicates so the class doesn't
/// pull in <c>WorldSession</c>, <c>PlayerMovementController</c>, or
/// any GameWindow-internal types — the unit test wires them to plain
/// boolean fields.
/// Production preconditions come from focused runtime owners rather than the
/// window host. The public delegate constructor remains a narrow test seam for
/// plain boolean fixtures.
/// </para>
/// </summary>
internal interface IPlayerModeAutoEntryContext
{
bool IsLiveInWorld { get; }
bool IsPlayerEntityPresent { get; }
bool IsPlayerControllerReady { get; }
bool IsWorldReady { get; }
bool IsPlayerModeActive { get; }
void EnterPlayerMode();
}
/// <summary>Production auto-entry context over canonical runtime owners.</summary>
internal sealed class LivePlayerModeAutoEntryContext
: IPlayerModeAutoEntryContext
{
private readonly ILiveInWorldSource _session;
private readonly LiveEntityRuntime _liveEntities;
private readonly ILocalPlayerIdentitySource _identity;
private readonly WorldRevealCoordinator _worldReveal;
private readonly ILocalPlayerModeSource _mode;
private readonly PlayerModeController _playerMode;
public LivePlayerModeAutoEntryContext(
ILiveInWorldSource session,
LiveEntityRuntime liveEntities,
ILocalPlayerIdentitySource identity,
WorldRevealCoordinator worldReveal,
ILocalPlayerModeSource mode,
PlayerModeController playerMode)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
_liveEntities = liveEntities ?? throw new ArgumentNullException(nameof(liveEntities));
_identity = identity ?? throw new ArgumentNullException(nameof(identity));
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
_playerMode = playerMode ?? throw new ArgumentNullException(nameof(playerMode));
}
public bool IsLiveInWorld => _session.IsInWorld;
public bool IsPlayerEntityPresent =>
_liveEntities.MaterializedWorldEntities.ContainsKey(_identity.ServerGuid);
public bool IsPlayerControllerReady => true;
public bool IsWorldReady =>
_liveEntities.TryGetSnapshot(
_identity.ServerGuid,
out AcDream.Core.Net.WorldSession.EntitySpawn player)
&& player.Position is { LandblockId: not 0u } position
&& _worldReveal.Evaluate(position.LandblockId).IsReady;
public bool IsPlayerModeActive => _mode.IsPlayerMode;
public void EnterPlayerMode() => _playerMode.EnterFromAutoEntry();
}
public sealed class PlayerModeAutoEntry
{
private readonly Func<bool> _isLiveInWorld;
private readonly Func<bool> _isPlayerEntityPresent;
private readonly Func<bool> _isPlayerControllerReady;
private readonly Func<bool> _isWorldReady;
private readonly Func<bool> _isPlayerModeActive;
private readonly Action _enterPlayerMode;
private sealed class DelegateContext : IPlayerModeAutoEntryContext
{
private readonly Func<bool> _isLiveInWorld;
private readonly Func<bool> _isPlayerEntityPresent;
private readonly Func<bool> _isPlayerControllerReady;
private readonly Func<bool> _isWorldReady;
private readonly Action _enterPlayerMode;
private readonly Func<bool> _isPlayerModeActive;
public DelegateContext(
Func<bool> isLiveInWorld,
Func<bool> isPlayerEntityPresent,
Func<bool> isPlayerControllerReady,
Func<bool> isWorldReady,
Action enterPlayerMode,
Func<bool>? isPlayerModeActive)
{
_isLiveInWorld = isLiveInWorld
?? throw new ArgumentNullException(nameof(isLiveInWorld));
_isPlayerEntityPresent = isPlayerEntityPresent
?? throw new ArgumentNullException(nameof(isPlayerEntityPresent));
_isPlayerControllerReady = isPlayerControllerReady
?? throw new ArgumentNullException(nameof(isPlayerControllerReady));
_isWorldReady = isWorldReady
?? throw new ArgumentNullException(nameof(isWorldReady));
_enterPlayerMode = enterPlayerMode
?? throw new ArgumentNullException(nameof(enterPlayerMode));
_isPlayerModeActive = isPlayerModeActive ?? (() => false);
}
public bool IsLiveInWorld => _isLiveInWorld();
public bool IsPlayerEntityPresent => _isPlayerEntityPresent();
public bool IsPlayerControllerReady => _isPlayerControllerReady();
public bool IsWorldReady => _isWorldReady();
public bool IsPlayerModeActive => _isPlayerModeActive();
public void EnterPlayerMode() => _enterPlayerMode();
}
private readonly IPlayerModeAutoEntryContext _context;
private bool _armed;
@ -72,20 +163,24 @@ public sealed class PlayerModeAutoEntry
/// player transition). Must construct the controller + chase
/// camera and switch the active camera; the auto-entry doesn't
/// reach inside.</param>
internal PlayerModeAutoEntry(IPlayerModeAutoEntryContext context) =>
_context = context ?? throw new ArgumentNullException(nameof(context));
public PlayerModeAutoEntry(
Func<bool> isLiveInWorld,
Func<bool> isPlayerEntityPresent,
Func<bool> isPlayerControllerReady,
Func<bool> isWorldReady,
Action enterPlayerMode,
Action enterPlayerMode,
Func<bool>? isPlayerModeActive = null)
: this(new DelegateContext(
isLiveInWorld,
isPlayerEntityPresent,
isPlayerControllerReady,
isWorldReady,
enterPlayerMode,
isPlayerModeActive))
{
_isLiveInWorld = isLiveInWorld ?? throw new ArgumentNullException(nameof(isLiveInWorld));
_isPlayerEntityPresent = isPlayerEntityPresent ?? throw new ArgumentNullException(nameof(isPlayerEntityPresent));
_isPlayerControllerReady = isPlayerControllerReady ?? throw new ArgumentNullException(nameof(isPlayerControllerReady));
_isWorldReady = isWorldReady ?? throw new ArgumentNullException(nameof(isWorldReady));
_enterPlayerMode = enterPlayerMode ?? throw new ArgumentNullException(nameof(enterPlayerMode));
_isPlayerModeActive = isPlayerModeActive ?? (() => false);
}
/// <summary>True iff <see cref="TryEnter"/> would still fire if the
@ -115,18 +210,18 @@ public sealed class PlayerModeAutoEntry
public bool TryEnter()
{
if (!_armed) return false;
if (_isPlayerModeActive())
if (_context.IsPlayerModeActive)
{
_armed = false;
return false;
}
if (!_isLiveInWorld()) return false;
if (!_isPlayerEntityPresent()) return false;
if (!_isPlayerControllerReady()) return false;
if (!_isWorldReady()) return false;
if (!_context.IsLiveInWorld) return false;
if (!_context.IsPlayerEntityPresent) return false;
if (!_context.IsPlayerControllerReady) return false;
if (!_context.IsWorldReady) return false;
_armed = false;
_enterPlayerMode();
_context.EnterPlayerMode();
return true;
}
}