using System; using AcDream.App.Net; using AcDream.App.Streaming; using AcDream.App.World; namespace AcDream.App.Input; /// /// 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. /// /// /// Why is this its own class? The auto-entry has four independent /// preconditions (live session reaches InWorld, 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. /// /// /// /// The public surface is: /// /// — call after EnterWorld succeeds to /// arm the entry trigger. /// — call when the user manually enters /// fly mode (or any other code path that pre-empts the auto-entry). /// — 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. /// /// /// /// /// 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. /// /// internal interface IPlayerModeAutoEntryContext { bool IsLiveInWorld { get; } bool IsPlayerEntityPresent { get; } bool IsPlayerControllerReady { get; } bool IsWorldReady { get; } bool IsPlayerModeActive { get; } void EnterPlayerMode(); } /// Production auto-entry context over canonical runtime owners. 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 _isLiveInWorld; private readonly Func _isPlayerEntityPresent; private readonly Func _isPlayerControllerReady; private readonly Func _isWorldReady; private readonly Action _enterPlayerMode; private readonly Func _isPlayerModeActive; public DelegateContext( Func isLiveInWorld, Func isPlayerEntityPresent, Func isPlayerControllerReady, Func isWorldReady, Action enterPlayerMode, Func? 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; /// /// Build an auto-entry guard. /// /// True iff the live session is in the /// InWorld state. Skip auto-entry when the session is null /// or hasn't reached InWorld yet. /// True iff the player's /// server-guid is already in the local entity dictionary (server /// has streamed at least one CreateObject for the character). /// 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. /// 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. /// 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. internal PlayerModeAutoEntry(IPlayerModeAutoEntryContext context) => _context = context ?? throw new ArgumentNullException(nameof(context)); public PlayerModeAutoEntry( Func isLiveInWorld, Func isPlayerEntityPresent, Func isPlayerControllerReady, Func isWorldReady, Action enterPlayerMode, Func? isPlayerModeActive = null) : this(new DelegateContext( isLiveInWorld, isPlayerEntityPresent, isPlayerControllerReady, isWorldReady, enterPlayerMode, isPlayerModeActive)) { } /// True iff would still fire if the /// preconditions become true. Flips false on a successful entry /// (one-shot) or when is invoked. public bool IsArmed => _armed; /// /// Arm the trigger. Call after WorldSession.EnterWorld /// returns successfully. Calling again while already armed is a /// no-op. /// public void Arm() => _armed = true; /// /// 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. /// public void Cancel() => _armed = false; /// /// Guard tick. If the trigger is armed AND every precondition is /// satisfied, invokes enterPlayerMode, disarms, and /// returns true. Returns false otherwise (no side effects). /// 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; } }