using System; namespace AcDream.App.Input; /// /// Phase K.2 — one-shot guard that auto-enters player mode after a /// successful login once every prerequisite is satisfied. Owned by /// GameWindow and ticked each frame from OnUpdate. /// /// /// 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 terrain under the /// spawn position has streamed in) 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. /// /// /// /// /// All preconditions are passed in as predicates so the class doesn't /// pull in WorldSession, PlayerMovementController, or /// any GameWindow-internal types — the unit test wires them to plain /// boolean fields. /// /// public sealed class PlayerModeAutoEntry { private readonly Func _isLiveInWorld; private readonly Func _isPlayerEntityPresent; private readonly Func _isPlayerControllerReady; private readonly Func _isSpawnGroundReady; private readonly Action _enterPlayerMode; 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 terrain under the /// player's spawn position has streamed into the physics engine. /// #106 gate-2 (2026-06-09): entering player mode earlier integrates /// gravity against an empty world and free-falls the player into the /// void. Retail never has this state — it loads cells synchronously; /// this hold is the async-streaming equivalent of that invariant. /// 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. public PlayerModeAutoEntry( Func isLiveInWorld, Func isPlayerEntityPresent, Func isPlayerControllerReady, Func isSpawnGroundReady, Action enterPlayerMode) { _isLiveInWorld = isLiveInWorld ?? throw new ArgumentNullException(nameof(isLiveInWorld)); _isPlayerEntityPresent = isPlayerEntityPresent ?? throw new ArgumentNullException(nameof(isPlayerEntityPresent)); _isPlayerControllerReady = isPlayerControllerReady ?? throw new ArgumentNullException(nameof(isPlayerControllerReady)); _isSpawnGroundReady = isSpawnGroundReady ?? throw new ArgumentNullException(nameof(isSpawnGroundReady)); _enterPlayerMode = enterPlayerMode ?? throw new ArgumentNullException(nameof(enterPlayerMode)); } /// 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 (!_isLiveInWorld()) return false; if (!_isPlayerEntityPresent()) return false; if (!_isPlayerControllerReady()) return false; if (!_isSpawnGroundReady()) return false; _armed = false; _enterPlayerMode(); return true; } }