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;
}
}