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.
231 lines
9.2 KiB
C#
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;
|
|
}
|
|
}
|