acdream/src/AcDream.App/Input/PlayerModeAutoEntry.cs
Erik 420e5eea70 refactor(app): key live projections by runtime identity
Move materialized live-object sidecars and presentation worksets to exact RuntimeEntityKey ownership. Runtime remains the only GUID/incarnation/local-ID authority while hydration, animation, effects, lights, equipped children, renderer resources, visibility, liveness, and teardown resolve exact projection identities. Preserve synchronous callbacks, local-ID allocation order, and current rendering behavior.
2026-07-25 21:50:58 +02:00

231 lines
9.2 KiB
C#

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. The update-frame
/// orchestrator ticks it through a typed production context.
///
/// <para>
/// Why is this its own class? The auto-entry has four independent
/// preconditions (live session reaches <c>InWorld</c>, the player
/// entity has been streamed into the world dictionary, the player
/// movement controller is constructible, and the initial world is fully
/// drawable and collidable) plus a manual-override path
/// (the user can flip into fly mode before the auto-entry fires —
/// their choice wins). All five interact with each other in a way
/// that's painful to test through GameWindow but trivial here against
/// fakes.
/// </para>
///
/// <para>
/// The public surface is:
/// <list type="bullet">
/// <item><see cref="Arm"/> — call after <c>EnterWorld</c> succeeds to
/// arm the entry trigger.</item>
/// <item><see cref="Cancel"/> — call when the user manually enters
/// fly mode (or any other code path that pre-empts the auto-entry).</item>
/// <item><see cref="TryEnter"/> — call once per frame; runs the
/// guard and fires the entry callback when armed AND every
/// precondition is satisfied; returns true on the firing tick.</item>
/// </list>
/// </para>
///
/// <para>
/// 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.ContainsWorldEntity(_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();
_worldReveal.Complete();
}
}
public sealed class PlayerModeAutoEntry
{
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;
/// <summary>
/// Build an auto-entry guard.
/// </summary>
/// <param name="isLiveInWorld">True iff the live session is in the
/// <c>InWorld</c> state. Skip auto-entry when the session is null
/// or hasn't reached InWorld yet.</param>
/// <param name="isPlayerEntityPresent">True iff the player's
/// server-guid is already in the local entity dictionary (server
/// has streamed at least one CreateObject for the character).</param>
/// <param name="isPlayerControllerReady">True iff the per-frame
/// PlayerMovementController is set up. Stays true once player mode
/// is established; the auto-entry's job is to flip it from false
/// to true exactly once.</param>
/// <param name="isWorldReady">True iff the player's destination is
/// complete across render publication, composite textures, and collision.
/// Retail keeps position completion behind one blocking cell-load edge;
/// acdream's asynchronous domains must converge before entry.</param>
/// <param name="enterPlayerMode">Action invoked on the firing
/// tick. The same routine the manual Tab handler invokes (fly →
/// 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,
Func<bool>? isPlayerModeActive = null)
: this(new DelegateContext(
isLiveInWorld,
isPlayerEntityPresent,
isPlayerControllerReady,
isWorldReady,
enterPlayerMode,
isPlayerModeActive))
{
}
/// <summary>True iff <see cref="TryEnter"/> would still fire if the
/// preconditions become true. Flips false on a successful entry
/// (one-shot) or when <see cref="Cancel"/> is invoked.</summary>
public bool IsArmed => _armed;
/// <summary>
/// Arm the trigger. Call after <c>WorldSession.EnterWorld</c>
/// returns successfully. Calling again while already armed is a
/// no-op.
/// </summary>
public void Arm() => _armed = true;
/// <summary>
/// Disarm the trigger without firing the callback. Call when the
/// user has manually entered fly mode (or any other code path
/// that pre-empts the auto-entry) — the user's choice wins.
/// </summary>
public void Cancel() => _armed = false;
/// <summary>
/// Guard tick. If the trigger is armed AND every precondition is
/// satisfied, invokes <c>enterPlayerMode</c>, disarms, and
/// returns true. Returns false otherwise (no side effects).
/// </summary>
public bool TryEnter()
{
if (!_armed) return false;
if (_context.IsPlayerModeActive)
{
_armed = false;
return false;
}
if (!_context.IsLiveInWorld) return false;
if (!_context.IsPlayerEntityPresent) return false;
if (!_context.IsPlayerControllerReady) return false;
if (!_context.IsWorldReady) return false;
_armed = false;
_context.EnterPlayerMode();
return true;
}
}