feat(G.3a): TeleportArrivalController hold-until-hydration state machine (#133)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c9650bd3bd
commit
7947d7ad0a
2 changed files with 226 additions and 0 deletions
103
src/AcDream.App/World/TeleportArrivalController.cs
Normal file
103
src/AcDream.App/World/TeleportArrivalController.cs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.World;
|
||||
|
||||
/// <summary>Verdict from the per-frame readiness probe for a held teleport arrival.</summary>
|
||||
public enum ArrivalReadiness
|
||||
{
|
||||
/// <summary>Destination not yet hydrated; keep holding.</summary>
|
||||
NotReady,
|
||||
|
||||
/// <summary>Destination terrain + cell are ready; place now.</summary>
|
||||
Ready,
|
||||
|
||||
/// <summary>The claim can never hydrate (e.g. an indoor cell id outside the dat's
|
||||
/// LandBlockInfo.NumCells range). Place immediately via the caller's safety-net
|
||||
/// demote rather than hold forever.</summary>
|
||||
Impossible,
|
||||
}
|
||||
|
||||
/// <summary>Lifecycle of a single teleport arrival.</summary>
|
||||
public enum TeleportArrivalPhase { Idle, Holding }
|
||||
|
||||
/// <summary>
|
||||
/// G.3a (#133) — holds a teleport arrival in portal space until the destination
|
||||
/// dungeon landblock/cell has streamed in, THEN places the player. Replaces the
|
||||
/// unconditional snap in <c>GameWindow.OnLivePositionUpdated</c> that resolved the
|
||||
/// arrival against the resident (old) landblocks before the destination hydrated
|
||||
/// and landed the player in ocean.
|
||||
///
|
||||
/// <para>The controller is pure: readiness and placement are injected delegates,
|
||||
/// so it carries no GL / dat / network dependency and is fully unit-testable. The
|
||||
/// player stays input-frozen while this is Holding because the GameWindow keeps
|
||||
/// <c>PlayerState.PortalSpace</c> until the placement delegate flips it back to
|
||||
/// InWorld.</para>
|
||||
///
|
||||
/// <para>The timeout is a coarse frame count (not wall-clock) so the controller
|
||||
/// needs no external clock; it is a loud safety net for a never-hydrating
|
||||
/// destination, not a precise deadline.</para>
|
||||
/// </summary>
|
||||
public sealed class TeleportArrivalController
|
||||
{
|
||||
/// <summary>~10 s at 60 fps. Coarse safety net for a destination that never streams.</summary>
|
||||
public const int DefaultMaxHoldFrames = 600;
|
||||
|
||||
private readonly Func<Vector3, uint, ArrivalReadiness> _readiness;
|
||||
private readonly Action<Vector3, uint, bool> _place; // (destPos, destCell, forced)
|
||||
private readonly int _maxHoldFrames;
|
||||
|
||||
private Vector3 _destPos;
|
||||
private uint _destCell;
|
||||
private int _heldFrames;
|
||||
|
||||
public TeleportArrivalPhase Phase { get; private set; } = TeleportArrivalPhase.Idle;
|
||||
|
||||
public TeleportArrivalController(
|
||||
Func<Vector3, uint, ArrivalReadiness> readiness,
|
||||
Action<Vector3, uint, bool> place,
|
||||
int maxHoldFrames = DefaultMaxHoldFrames)
|
||||
{
|
||||
_readiness = readiness ?? throw new ArgumentNullException(nameof(readiness));
|
||||
_place = place ?? throw new ArgumentNullException(nameof(place));
|
||||
_maxHoldFrames = maxHoldFrames;
|
||||
}
|
||||
|
||||
/// <summary>Begin holding for a teleport arrival. Called from OnLivePositionUpdated
|
||||
/// AFTER the streaming origin has been recentered on the destination landblock.
|
||||
/// Re-calling with a fresh server position resets the hold (server-authoritative).</summary>
|
||||
public void BeginArrival(Vector3 destPos, uint destCell)
|
||||
{
|
||||
_destPos = destPos;
|
||||
_destCell = destCell;
|
||||
_heldFrames = 0;
|
||||
Phase = TeleportArrivalPhase.Holding;
|
||||
}
|
||||
|
||||
/// <summary>Per-frame: evaluate readiness and place when ready / impossible / timed out.
|
||||
/// No-op when Idle.</summary>
|
||||
public void Tick()
|
||||
{
|
||||
if (Phase != TeleportArrivalPhase.Holding) return;
|
||||
_heldFrames++;
|
||||
|
||||
ArrivalReadiness verdict = _readiness(_destPos, _destCell);
|
||||
if (verdict == ArrivalReadiness.Ready)
|
||||
{
|
||||
Place(forced: false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (verdict == ArrivalReadiness.Impossible || _heldFrames >= _maxHoldFrames)
|
||||
{
|
||||
Place(forced: true);
|
||||
}
|
||||
// else NotReady -> keep holding
|
||||
}
|
||||
|
||||
private void Place(bool forced)
|
||||
{
|
||||
_place(_destPos, _destCell, forced);
|
||||
Phase = TeleportArrivalPhase.Idle;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue