refactor(runtime): own teleport destination correlation

This commit is contained in:
Erik 2026-07-26 17:52:34 +02:00
parent 38b3773cb9
commit 6a063a27d4
23 changed files with 1209 additions and 487 deletions

View file

@ -552,6 +552,79 @@ no Marketplace-specific classifier or portal-exit behavior is required.
consumed even if the channel already advanced. Thus reordered delivery and
retransmitted notifications cannot materialize the wrong destination or
strand the transit, while canonical physics retains its own retail gates.
### Runtime F751/Position ownership pseudocode (Slice J6.3)
Named retail `SmartBox::HandlePlayerTeleport @ 0x00452150` compares the
incoming F751 sequence with the player's current `update_times[4]`
(`TELEPORT_TS`) using the retail unsigned-16 wrap rule. An older notification
is rejected. Equality or a newer notification clears
`position_update_complete` and `has_been_teleported`, then sets
`waiting_for_teleport`; it does **not** write `update_times[4]`.
`SmartBox::TeleportPlayer @ 0x00453910` later installs the complete accepted
`Position` and calls `PlayerPositionUpdated`. ACE corroborates the wire
transaction: `GameMessagePlayerTeleport` advances ObjectTeleport once, then
the immediately following UpdatePosition carries that same value. Holtburger
confirms F751 is only one little-endian `u16` plus alignment.
The Runtime ownership port therefore keeps canonical physics admission and
transit correlation separate:
```text
on F751(sequence):
reject unless canonical player TELEPORT_TS admits equality/newer
reject if this Runtime transit owner already observed sequence or a newer one
retire buffered destinations older than sequence
end the prior active correlation, retaining wrap-aware history
remember sequence as a pending teleport start
when the graphical host can enter portal presentation:
promote pending sequence to active sequence
if an accepted matching Position was buffered:
expose it as the one destination waiting to be aimed
on accepted local-player Position(destination, teleportAdvanced):
canonical physics has already admitted and applied the packet
if teleportAdvanced:
retire buffered destinations older than destination.TeleportSequence
if active sequence equals destination.TeleportSequence:
accept only the first destination
else if pending sequence equals destination.TeleportSequence:
buffer only the first destination until presentation activates
else if teleportAdvanced and the sequence may belong to a future F751:
buffer only the first destination
else:
ignore it for transit correlation
when App aims a destination:
Runtime atomically verifies:
active teleport
exact teleport sequence
exact accepted destination cell
Runtime creates the one reveal generation and consumes the aim claim
App translates the cell-local frame into render space and recenters
before and after App placement:
verify the same reveal generation + teleport sequence + destination cell
only then acknowledge Runtime materialization
on ordinary portal completion:
end active correlation but retain last-F751 history
on session reset:
clear pending/active correlation, buffered/accepted destinations,
last-F751 history, and the visible reveal snapshot
retain the monotonic reveal-generation counter
```
The Position-before-F751 buffer is not a second position authority. It stores
only immutable data from a Position already accepted by the canonical Runtime
physics owner, and exists because host projection availability and ordered
event delivery can straddle the presentation activation edge. It can neither
apply physics nor turn an ordinary non-advancing Position into a portal
destination.
- Destination placement resets the retail chase camera's published and sought
positions to the player before the normal update path resumes.
- `RetailLiveFrameCoordinator` owns the `SmartBox::UseTime` phase barrier:

View file

@ -741,6 +741,7 @@ internal sealed class SessionPlayerCompositionPhase
streamingOriginRecenter,
streaming,
sealedDungeonCells),
live.WorldTransit,
worldReveal,
new LocalPlayerTeleportPlacement(
d.PhysicsEngine,

View file

@ -5,6 +5,7 @@ using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Content;
@ -1764,7 +1765,8 @@ internal sealed class LiveEntityNetworkUpdateController
&& update.Guid == _playerServerGuid)
{
_localPlayerTeleport.OfferDestination(
update,
RuntimeTeleportDestinationAdapter.FromAcceptedPosition(
update),
timestamps.TeleportAdvanced);
}
}

View file

@ -5,12 +5,12 @@ using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Rendering;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Streaming;
@ -19,7 +19,7 @@ internal interface ILocalPlayerTeleportNetworkSink
void OnTeleportStarted(uint sequence);
void OfferDestination(
WorldSession.EntityPositionUpdate update,
RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced);
void ResetSession();
@ -56,9 +56,9 @@ internal sealed class DeferredLocalPlayerTeleportNetworkSink
public void OnTeleportStarted(uint sequence) => Required().OnTeleportStarted(sequence);
public void OfferDestination(
WorldSession.EntityPositionUpdate update,
RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced) =>
Required().OfferDestination(update, teleportTimestampAdvanced);
Required().OfferDestination(destination, teleportTimestampAdvanced);
public void ResetSession() => Required().ResetSession();
@ -372,9 +372,10 @@ internal sealed class LocalPlayerTeleportPresentation
}
/// <summary>
/// Single update-thread owner of the local F751/Position-correlated portal
/// lifetime: activation, destination aim, readiness hold, Place, viewport
/// transitions, LoginComplete, and session/reset symmetry.
/// Graphical-host orchestrator for Runtime's local F751/Position-correlated
/// portal lifetime. Runtime owns sequence/destination/reveal authority; this
/// update-thread adapter owns portal viewport activation, render-space aim,
/// readiness preparation, placement callbacks, and LoginComplete presentation.
/// </summary>
internal sealed class LocalPlayerTeleportController
: ILocalPlayerTeleportFramePhase,
@ -382,7 +383,7 @@ internal sealed class LocalPlayerTeleportController
AcDream.App.Interaction.ISelectionViewPlaneSource,
IDisposable
{
private readonly TeleportTransitCoordinator<WorldSession.EntityPositionUpdate> _transit = new();
private readonly RuntimeWorldTransitState _transit;
private readonly ILocalPlayerTeleportAuthority _authority;
private readonly ILocalPlayerTeleportInputLifetime _input;
private readonly ILocalPlayerTeleportModeOperations _mode;
@ -395,8 +396,7 @@ internal sealed class LocalPlayerTeleportController
private Vector3 _pendingPosition;
private uint _pendingCell;
private Quaternion _pendingRotation = Quaternion.Identity;
private bool _hasAcceptedDestination;
private WorldSession.EntityPositionUpdate _acceptedDestination;
private long _pendingRevealGeneration;
private float _holdSeconds;
private long _lifetimeGeneration;
private bool _disposed;
@ -406,6 +406,7 @@ internal sealed class LocalPlayerTeleportController
ILocalPlayerTeleportInputLifetime input,
ILocalPlayerTeleportModeOperations mode,
ILocalPlayerTeleportStreamingOperations streaming,
RuntimeWorldTransitState transit,
WorldRevealCoordinator worldReveal,
ILocalPlayerTeleportPlacement placement,
ILocalPlayerTeleportSession session,
@ -415,22 +416,24 @@ internal sealed class LocalPlayerTeleportController
_input = input ?? throw new ArgumentNullException(nameof(input));
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
_placement = placement ?? throw new ArgumentNullException(nameof(placement));
_session = session ?? throw new ArgumentNullException(nameof(session));
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
}
public bool IsActive => _transit.IsActive;
public bool IsActive => _transit.IsTeleportActive;
public bool IsPortalViewportVisible => _presentation.IsPortalViewportVisible;
public uint ActiveDestinationCell => _transit.IsActive ? _pendingCell : 0u;
public uint ActiveDestinationCell =>
_transit.IsTeleportActive ? _pendingCell : 0u;
public void OnTeleportStarted(uint sequence)
{
ThrowIfDisposed();
ushort teleportSequence = (ushort)sequence;
if (!_authority.IsFreshStart(teleportSequence)
|| !_transit.CanBegin(teleportSequence))
|| !_transit.CanQueueTeleportStart(teleportSequence))
{
return;
}
@ -444,24 +447,21 @@ internal sealed class LocalPlayerTeleportController
if (_lifetimeGeneration != resetGeneration)
return;
_transit.QueueStart(teleportSequence);
if (!_transit.TryQueueTeleportStart(teleportSequence))
return;
TryActivatePendingPresentation();
Console.WriteLine($"live: teleport queued (seq={sequence})");
}
public void OfferDestination(
WorldSession.EntityPositionUpdate update,
RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced)
{
ThrowIfDisposed();
if (_transit.OfferDestination(
update.TeleportSequence,
teleportTimestampAdvanced,
update,
out WorldSession.EntityPositionUpdate destination))
{
AcceptDestination(destination);
}
_transit.OfferTeleportDestination(
destination,
teleportTimestampAdvanced);
TryAimAcceptedDestination();
}
public void Tick(float deltaSeconds)
@ -469,11 +469,11 @@ internal sealed class LocalPlayerTeleportController
ThrowIfDisposed();
TryActivatePendingPresentation();
TryAimAcceptedDestination();
if (!_transit.IsActive)
if (!_transit.IsTeleportActive)
return;
long generation = _lifetimeGeneration;
ushort sequence = _transit.Sequence;
ushort sequence = _transit.ActiveTeleportSequence;
if (!_mode.TryEnterPortalSpace()
|| !IsCurrentLifetime(generation, sequence)
|| _mode.Controller is null)
@ -506,13 +506,23 @@ internal sealed class LocalPlayerTeleportController
switch (teleportEvent)
{
case TeleportAnimEvent.Place:
if (!_worldReveal.CanPlacePortalDestination(
_pendingRevealGeneration,
sequence,
_pendingCell))
{
return;
}
_placement.Place(
_pendingPosition,
_pendingCell,
_pendingRotation);
if (!IsCurrentLifetime(generation, sequence))
return;
_worldReveal.ObserveMaterialized();
_worldReveal.ObserveMaterialized(
_pendingRevealGeneration,
sequence,
_pendingCell);
if (!IsCurrentLifetime(generation, sequence))
return;
break;
@ -571,43 +581,43 @@ internal sealed class LocalPlayerTeleportController
private void TryActivatePendingPresentation()
{
if (!_transit.HasPendingStart)
if (!_transit.HasPendingTeleportStart)
return;
long generation = _lifetimeGeneration;
if (!_mode.TryEnterPortalSpace()
|| _lifetimeGeneration != generation
|| !_transit.HasPendingStart)
|| !_transit.HasPendingTeleportStart)
{
return;
}
_holdSeconds = 0f;
_presentation.Begin(_mode.Projection);
if (_lifetimeGeneration != generation || !_transit.HasPendingStart)
if (_lifetimeGeneration != generation
|| !_transit.HasPendingTeleportStart)
{
return;
bool hasBufferedDestination = _transit.Activate(out var destination);
if (hasBufferedDestination)
AcceptDestination(destination);
Console.WriteLine(
$"live: teleport presentation started (seq={_transit.Sequence})");
}
private void AcceptDestination(WorldSession.EntityPositionUpdate destination)
{
_acceptedDestination = destination;
_hasAcceptedDestination = true;
if (!_transit.ActivateQueuedTeleport())
return;
TryAimAcceptedDestination();
Console.WriteLine(
$"live: teleport presentation started "
+ $"(seq={_transit.ActiveTeleportSequence})");
}
private void TryAimAcceptedDestination()
{
if (!_hasAcceptedDestination || !_transit.IsActive)
if (!_transit.TryGetAcceptedTeleportDestination(
out RuntimeTeleportDestination destination))
{
return;
}
long generation = _lifetimeGeneration;
ushort sequence = _transit.Sequence;
ushort sequence = _transit.ActiveTeleportSequence;
PlayerMovementController? controller = _mode.Controller;
if (controller is null)
{
@ -622,26 +632,19 @@ internal sealed class LocalPlayerTeleportController
return;
}
WorldSession.EntityPositionUpdate destination = _acceptedDestination;
if (!AimDestination(destination, controller, generation, sequence))
return;
if (IsCurrentLifetime(generation, sequence))
{
_hasAcceptedDestination = false;
_acceptedDestination = default;
}
}
private bool AimDestination(
WorldSession.EntityPositionUpdate update,
RuntimeTeleportDestination destination,
PlayerMovementController controller,
long generation,
ushort sequence)
{
var position = update.Position;
int landblockX = (int)((position.LandblockId >> 24) & 0xFFu);
int landblockY = (int)((position.LandblockId >> 16) & 0xFFu);
Position position = destination.Position;
int landblockX = (int)((position.ObjCellId >> 24) & 0xFFu);
int landblockY = (int)((position.ObjCellId >> 16) & 0xFFu);
uint streamingOriginLandblockId = StreamingRegion.EncodeLandblockId(
_streaming.CenterX,
_streaming.CenterY);
@ -649,14 +652,11 @@ internal sealed class LocalPlayerTeleportController
(landblockX - _streaming.CenterX) * 192f,
(landblockY - _streaming.CenterY) * 192f,
0f);
var translated = new Vector3(
position.PositionX,
position.PositionY,
position.PositionZ) + origin;
Vector3 translated = position.Frame.Origin + origin;
TeleportLandblockTransition transition = TeleportLandblockTransition.Classify(
controller.CellId,
position.LandblockId,
position.ObjCellId,
streamingOriginLandblockId);
int oldX = (int)((transition.SourceLandblockId >> 24) & 0xFFu);
int oldY = (int)((transition.SourceLandblockId >> 16) & 0xFFu);
@ -669,7 +669,14 @@ internal sealed class LocalPlayerTeleportController
// Retail SmartBox enters blocking_for_cells before old-world object,
// physics, landscape, and ambient owners may advance again. Begin the
// reveal generation before a recenter can start detaching that world.
_worldReveal.Begin(RuntimePortalKind.Portal, position.LandblockId);
if (!_worldReveal.TryBeginPortal(
sequence,
position.ObjCellId,
out long revealGeneration))
{
return false;
}
_pendingRevealGeneration = revealGeneration;
if (!IsCurrentLifetime(generation, sequence))
return false;
@ -677,7 +684,7 @@ internal sealed class LocalPlayerTeleportController
if (transition.ChangesStreamingCenter)
{
bool isSealedDungeon = _streaming.IsSealedDungeon(
position.LandblockId);
position.ObjCellId);
if (!IsCurrentLifetime(generation, sequence))
return false;
@ -688,28 +695,24 @@ internal sealed class LocalPlayerTeleportController
if (!IsCurrentLifetime(generation, sequence))
return false;
worldPosition = new Vector3(
position.PositionX,
position.PositionY,
position.PositionZ);
position.Frame.Origin.X,
position.Frame.Origin.Y,
position.Frame.Origin.Z);
}
else
{
worldPosition = translated;
}
_pendingRotation = new Quaternion(
position.RotationX,
position.RotationY,
position.RotationZ,
position.RotationW);
_pendingRotation = position.Frame.Orientation;
_pendingPosition = worldPosition;
_pendingCell = position.LandblockId;
_pendingCell = position.ObjCellId;
_holdSeconds = 0f;
PhysicsDiagnostics.LogTeleport(
"AIM",
position.LandblockId,
$"seq={update.TeleportSequence} lb={landblockX},{landblockY} "
+ $"indoor={((position.LandblockId & 0xFFFFu) >= 0x0100u)} "
position.ObjCellId,
$"seq={destination.TeleportSequence} lb={landblockX},{landblockY} "
+ $"indoor={((position.ObjCellId & 0xFFFFu) >= 0x0100u)} "
+ $"playerCross={transition.CrossesLandblock} "
+ $"centerChange={transition.ChangesStreamingCenter}");
return true;
@ -718,20 +721,11 @@ internal sealed class LocalPlayerTeleportController
private long ResetTransit(bool clearSession)
{
long generation = checked(++_lifetimeGeneration);
if (clearSession)
{
_transit.ClearSession();
}
else
{
_transit.EndActive();
}
_pendingPosition = default;
_pendingCell = 0u;
_pendingRotation = Quaternion.Identity;
_hasAcceptedDestination = false;
_acceptedDestination = default;
_pendingRevealGeneration = 0;
_holdSeconds = 0f;
_streaming.ResetRecenter(clearSession);
@ -741,7 +735,10 @@ internal sealed class LocalPlayerTeleportController
if (clearSession)
_worldReveal.ResetSession();
else
{
_transit.EndTeleport();
_worldReveal.Cancel();
}
if (_lifetimeGeneration != generation)
return generation;
@ -754,8 +751,8 @@ internal sealed class LocalPlayerTeleportController
private bool IsCurrentLifetime(long generation, ushort sequence) =>
_lifetimeGeneration == generation
&& _transit.IsActive
&& _transit.Sequence == sequence;
&& _transit.IsTeleportActive
&& _transit.ActiveTeleportSequence == sequence;
private void ThrowIfDisposed() =>
ObjectDisposedException.ThrowIf(_disposed, this);

View file

@ -0,0 +1,37 @@
using System.Numerics;
using AcDream.Core.Net;
using AcDream.Core.Physics;
using AcDream.Runtime;
namespace AcDream.App.Streaming;
/// <summary>
/// One-way boundary from an already accepted wire Position into Runtime's
/// presentation-independent teleport destination. Canonical physics consumes
/// the packet first; this adapter neither gates nor applies it.
/// </summary>
internal static class RuntimeTeleportDestinationAdapter
{
public static RuntimeTeleportDestination FromAcceptedPosition(
in WorldSession.EntityPositionUpdate update)
{
var position = update.Position;
return new RuntimeTeleportDestination(
update.Guid,
update.InstanceSequence,
update.PositionSequence,
update.TeleportSequence,
update.ForcePositionSequence,
new Position(
position.LandblockId,
new Vector3(
position.PositionX,
position.PositionY,
position.PositionZ),
new Quaternion(
position.RotationX,
position.RotationY,
position.RotationZ,
position.RotationW)));
}
}

View file

@ -1,150 +0,0 @@
namespace AcDream.App.Streaming;
/// <summary>
/// Correlates F751 notifications with accepted Position packets without
/// imposing presentation order on canonical physics. F751 deliberately does
/// not consume retail's TELEPORT_TS, and the matching Position may be observed
/// on either side of the notification.
/// </summary>
internal sealed class TeleportTransitCoordinator<TDestination>
where TDestination : struct
{
private bool _hasLastStart;
private ushort _lastStartSequence;
private readonly Dictionary<ushort, TDestination> _bufferedDestinations = new();
private bool _hasPendingStart;
private ushort _pendingStartSequence;
private bool _destinationAccepted;
public bool IsActive { get; private set; }
public ushort Sequence { get; private set; }
public bool HasPendingStart => _hasPendingStart;
/// <summary>Reject retransmitted or older F751 notifications.</summary>
public bool CanBegin(ushort sequence) =>
!_hasLastStart || IsNewer(_lastStartSequence, sequence);
/// <summary>
/// Record a fresh notification lifetime. Presentation activation is a
/// separate edge because acdream's optional developer camera can
/// temporarily withdraw the normal player projection.
/// </summary>
public bool QueueStart(ushort sequence)
{
if (!CanBegin(sequence))
return false;
PruneDestinationsOlderThan(sequence);
IsActive = false;
Sequence = 0;
_hasPendingStart = true;
_pendingStartSequence = sequence;
_hasLastStart = true;
_lastStartSequence = sequence;
_destinationAccepted = false;
return true;
}
/// <summary>
/// Activate the queued presentation and consume a destination that was
/// accepted before the player projection became available.
/// </summary>
public bool Activate(out TDestination bufferedDestination)
{
bufferedDestination = default;
if (!_hasPendingStart)
return false;
IsActive = true;
Sequence = _pendingStartSequence;
_hasPendingStart = false;
_pendingStartSequence = 0;
_destinationAccepted = false;
if (!_bufferedDestinations.Remove(Sequence, out bufferedDestination))
return false;
_destinationAccepted = true;
return true;
}
/// <summary>
/// Offer an accepted Position packet. A matching active transit consumes
/// its first packet even when TELEPORT_TS was already advanced. Without a
/// matching notification, only a timestamp-advancing packet is buffered;
/// ordinary movement can never become a future portal destination.
/// </summary>
public bool OfferDestination(
ushort sequence,
bool teleportTimestampAdvanced,
TDestination destination,
out TDestination acceptedDestination)
{
acceptedDestination = default;
// An advancing TELEPORT_TS makes every older destination impossible
// to match a future F751. Retire those generations now so a skipped
// notification cannot retain a stale destination through u16 wrap.
if (teleportTimestampAdvanced)
PruneDestinationsOlderThan(sequence);
if (IsActive && sequence == Sequence)
{
if (_destinationAccepted)
return false;
_destinationAccepted = true;
acceptedDestination = destination;
return true;
}
if (_hasPendingStart && sequence == _pendingStartSequence)
{
_bufferedDestinations.TryAdd(sequence, destination);
return false;
}
ushort correlationSequence = _hasPendingStart
? _pendingStartSequence
: Sequence;
bool mayBelongToFutureStart = (!IsActive && !_hasPendingStart)
|| IsNewer(correlationSequence, sequence);
if (!teleportTimestampAdvanced || !mayBelongToFutureStart)
return false;
_bufferedDestinations.TryAdd(sequence, destination);
return false;
}
/// <summary>End presentation while retaining duplicate and reorder history.</summary>
public void EndActive()
{
IsActive = false;
Sequence = 0;
_hasPendingStart = false;
_pendingStartSequence = 0;
_destinationAccepted = false;
}
/// <summary>Clear every correlation value at a network-session boundary.</summary>
public void ClearSession()
{
EndActive();
_hasLastStart = false;
_lastStartSequence = 0;
_bufferedDestinations.Clear();
}
private static bool IsNewer(ushort current, ushort incoming)
=> AcDream.Core.Physics.PhysicsTimestampGate.IsNewer(current, incoming);
private void PruneDestinationsOlderThan(ushort sequence)
{
foreach (ushort bufferedSequence in _bufferedDestinations.Keys.ToArray())
{
if (IsNewer(bufferedSequence, sequence))
_bufferedDestinations.Remove(bufferedSequence);
}
}
}

View file

@ -77,7 +77,7 @@ internal sealed class WorldRevealCoordinator
_transit.Snapshot.PortalMaterializationCount;
public bool WaitCueShown => _transit.Snapshot.WaitCueShown;
public long Begin(RuntimePortalKind kind, uint destinationCell)
public long BeginLogin(uint destinationCell)
{
if (destinationCell == 0u)
throw new ArgumentOutOfRangeException(nameof(destinationCell));
@ -85,7 +85,45 @@ internal sealed class WorldRevealCoordinator
WorldGenerationQuiescenceEdge quiescenceEdge =
_quiescence?.CaptureBegin() ?? default;
_readiness.Begin();
long generation = _transit.BeginReveal(kind, destinationCell);
long generation = _transit.BeginLoginReveal(destinationCell);
BeginHostLifetime(
generation,
destinationCell,
quiescenceEdge);
return generation;
}
public bool TryBeginPortal(
ushort teleportSequence,
uint destinationCell,
out long generation)
{
if (destinationCell == 0u)
throw new ArgumentOutOfRangeException(nameof(destinationCell));
WorldGenerationQuiescenceEdge quiescenceEdge =
_quiescence?.CaptureBegin() ?? default;
if (!_transit.TryBeginPortalReveal(
teleportSequence,
destinationCell,
out generation))
{
return false;
}
_readiness.Begin();
BeginHostLifetime(
generation,
destinationCell,
quiescenceEdge);
return true;
}
private void BeginHostLifetime(
long generation,
uint destinationCell,
in WorldGenerationQuiescenceEdge quiescenceEdge)
{
_quiescence?.CommitBegin(quiescenceEdge);
_reservationGeneration = generation;
_worldViewportReleased = false;
@ -94,7 +132,6 @@ internal sealed class WorldRevealCoordinator
destinationCell,
WorldRevealReadinessBarrier.RequiredRenderRadius(destinationCell));
_renderResources?.BeginDestinationReveal(generation);
return generation;
}
/// <summary>
@ -138,17 +175,27 @@ internal sealed class WorldRevealCoordinator
/// <c>TAS_TUNNEL_CONTINUE</c>. The normal world viewport is not restored
/// until the later <c>TUNNEL_FADE_OUT -&gt; WORLD_FADE_IN</c> edge.
/// </remarks>
public void ObserveMaterialized()
{
RuntimePortalSnapshot snapshot = _transit.Snapshot;
if (snapshot.Generation == 0)
return;
public bool CanPlacePortalDestination(
long generation,
ushort teleportSequence,
uint destinationCell) =>
_transit.CanPlacePortalDestination(
generation,
teleportSequence,
destinationCell);
public bool ObserveMaterialized(
long generation,
ushort teleportSequence,
uint destinationCell)
{
bool wasAvailable = _transit.IsWorldSimulationAvailable;
_transit.AcknowledgeMaterialized(
snapshot.Generation,
snapshot.DestinationCell);
bool acknowledged = _transit.AcknowledgePortalMaterialized(
generation,
teleportSequence,
destinationCell);
ObserveSimulationRelease(wasAvailable);
return acknowledged;
}
public void ObserveWorldViewportVisible()

View file

@ -238,7 +238,7 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
lbY,
isSealedDungeon: _sealedDungeonCells.IsSealedDungeon(
position.LandblockId));
_worldReveal.Begin(RuntimePortalKind.Login, position.LandblockId);
_worldReveal.BeginLogin(position.LandblockId);
return new(
true,

View file

@ -92,6 +92,23 @@ public enum RuntimePortalKind
Portal,
}
/// <summary>
/// Immutable, presentation-independent destination from one canonical
/// accepted local-player Position packet. The frame remains cell-local;
/// graphical hosts may translate it into their current render origin, while
/// headless hosts consume the same full retail cell identity directly.
/// </summary>
public readonly record struct RuntimeTeleportDestination(
uint EntityGuid,
ushort InstanceSequence,
ushort PositionSequence,
ushort TeleportSequence,
ushort ForcePositionSequence,
Position Position)
{
public uint CellId => Position.ObjCellId;
}
public readonly record struct RuntimeDestinationReadiness(
long Generation,
uint DestinationCell,

View file

@ -3,13 +3,16 @@ using System.Globalization;
namespace AcDream.Runtime.World;
/// <summary>
/// Canonical presentation-independent owner of one login/portal reveal
/// generation. The graphical or headless host supplies typed destination
/// readiness acknowledgements; Runtime owns which generation and destination
/// those acknowledgements are allowed to advance.
/// Canonical presentation-independent owner of local F751/Position
/// correlation and one login/portal reveal generation. The graphical or
/// headless host supplies typed destination readiness acknowledgements;
/// Runtime owns which teleport sequence, generation, and destination those
/// acknowledgements are allowed to advance.
/// </summary>
/// <remarks>
/// Retail ordering follows <c>SmartBox::TeleportPlayer @ 0x00453910</c>,
/// Retail ordering follows
/// <c>SmartBox::HandlePlayerTeleport @ 0x00452150</c>,
/// <c>SmartBox::TeleportPlayer @ 0x00453910</c>,
/// <c>SmartBox::UseTime @ 0x00455410</c>, and
/// <c>gmSmartBoxUI::EndTeleportAnimation @ 0x004D65A0</c>. Destination
/// materialization resumes simulation before the later normal-world viewport
@ -22,8 +25,19 @@ public sealed class RuntimeWorldTransitState
TimeSpan.FromSeconds(5);
private readonly Action<string> _log;
private readonly Dictionary<ushort, RuntimeTeleportDestination>
_bufferedDestinations = [];
private RuntimePortalSnapshot _snapshot = RuntimePortalSnapshot.Idle;
private long _nextGeneration;
private bool _hasLastTeleportStart;
private ushort _lastTeleportStartSequence;
private bool _hasPendingTeleportStart;
private ushort _pendingTeleportStartSequence;
private bool _teleportActive;
private ushort _activeTeleportSequence;
private bool _destinationAccepted;
private bool _hasAcceptedDestination;
private RuntimeTeleportDestination _acceptedDestination;
public RuntimeWorldTransitState(Action<string>? log = null)
{
@ -33,10 +47,70 @@ public sealed class RuntimeWorldTransitState
public RuntimePortalSnapshot Snapshot => _snapshot;
public bool IsWorldSimulationAvailable =>
_snapshot.WorldSimulationAvailable;
public bool IsTeleportActive => _teleportActive;
public ushort ActiveTeleportSequence =>
_teleportActive ? _activeTeleportSequence : (ushort)0;
public bool HasPendingTeleportStart => _hasPendingTeleportStart;
public bool HasAcceptedTeleportDestination => _hasAcceptedDestination;
public int BufferedTeleportDestinationCount =>
_bufferedDestinations.Count;
public int DiagnosticFailureCount { get; private set; }
public Exception? LastDiagnosticFailure { get; private set; }
public long BeginReveal(
/// <summary>
/// Starts initial-login reveal. Portal reveals must instead claim the
/// exact F751-correlated destination through
/// <see cref="TryBeginPortalReveal"/>.
/// </summary>
public long BeginLoginReveal(uint destinationCell) =>
BeginRevealCore(RuntimePortalKind.Login, destinationCell);
/// <summary>
/// Atomically claims the one accepted Position paired with the active F751
/// sequence and starts its reveal generation.
/// </summary>
public bool TryBeginPortalReveal(
ushort teleportSequence,
uint destinationCell,
out long generation)
{
generation = 0;
if (!_teleportActive
|| teleportSequence != _activeTeleportSequence
|| !_hasAcceptedDestination)
{
LogRejected(
"portal-begin-without-destination",
$"active={_teleportActive} "
+ $"expectedSequence={_activeTeleportSequence} "
+ $"actualSequence={teleportSequence} "
+ $"accepted={_hasAcceptedDestination}");
return false;
}
RuntimeTeleportDestination destination = _acceptedDestination;
if (destination.TeleportSequence != teleportSequence
|| destination.CellId == 0u
|| destination.CellId != destinationCell)
{
FailInvariant(
"portal-begin-destination-mismatch",
$"sequence={teleportSequence} "
+ $"acceptedSequence={destination.TeleportSequence} "
+ $"expectedCell=0x{destination.CellId:X8} "
+ $"actualCell=0x{destinationCell:X8}");
return false;
}
generation = BeginRevealCore(
RuntimePortalKind.Portal,
destinationCell);
_hasAcceptedDestination = false;
_acceptedDestination = default;
return true;
}
private long BeginRevealCore(
RuntimePortalKind kind,
uint destinationCell)
{
@ -79,6 +153,151 @@ public sealed class RuntimeWorldTransitState
return generation;
}
/// <summary>
/// Returns whether an F751 sequence is newer than this transit owner's
/// already-observed notification history. Canonical player TELEPORT_TS is
/// checked independently by the Runtime entity/physics owner.
/// </summary>
public bool CanQueueTeleportStart(ushort sequence) =>
!_hasLastTeleportStart
|| IsNewer(_lastTeleportStartSequence, sequence);
/// <summary>
/// Records one fresh F751 notification without advancing canonical
/// TELEPORT_TS. Presentation activation is a separate host edge.
/// </summary>
public bool TryQueueTeleportStart(ushort sequence)
{
if (!CanQueueTeleportStart(sequence))
return false;
PruneDestinationsOlderThan(sequence);
_teleportActive = false;
_activeTeleportSequence = 0;
_hasPendingTeleportStart = true;
_pendingTeleportStartSequence = sequence;
_hasLastTeleportStart = true;
_lastTeleportStartSequence = sequence;
_destinationAccepted = false;
_hasAcceptedDestination = false;
_acceptedDestination = default;
return true;
}
/// <summary>
/// Promotes the queued F751 lifetime once its host can enter portal space.
/// A Position accepted before that host edge becomes the same active
/// destination; no position or physics state is applied here.
/// </summary>
public bool ActivateQueuedTeleport()
{
if (!_hasPendingTeleportStart)
return false;
_teleportActive = true;
_activeTeleportSequence = _pendingTeleportStartSequence;
_hasPendingTeleportStart = false;
_pendingTeleportStartSequence = 0;
_destinationAccepted = false;
_hasAcceptedDestination = false;
_acceptedDestination = default;
if (!_bufferedDestinations.Remove(
_activeTeleportSequence,
out RuntimeTeleportDestination buffered))
{
return true;
}
_destinationAccepted = true;
_hasAcceptedDestination = true;
_acceptedDestination = buffered;
return true;
}
/// <summary>
/// Offers a destination only after canonical Runtime physics has accepted
/// and applied its Position packet. Ordinary, non-advancing movement can
/// never become a future portal destination.
/// </summary>
public bool OfferTeleportDestination(
in RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced)
{
ushort sequence = destination.TeleportSequence;
if (destination.CellId == 0u)
return false;
if (teleportTimestampAdvanced)
PruneDestinationsOlderThan(sequence);
if (_teleportActive && sequence == _activeTeleportSequence)
{
if (_destinationAccepted)
return false;
_destinationAccepted = true;
_hasAcceptedDestination = true;
_acceptedDestination = destination;
return true;
}
if (_hasPendingTeleportStart
&& sequence == _pendingTeleportStartSequence)
{
_bufferedDestinations.TryAdd(sequence, destination);
return false;
}
ushort correlationSequence = _hasPendingTeleportStart
? _pendingTeleportStartSequence
: _activeTeleportSequence;
bool mayBelongToFutureStart =
(!_teleportActive && !_hasPendingTeleportStart)
|| IsNewer(correlationSequence, sequence);
if (!teleportTimestampAdvanced || !mayBelongToFutureStart)
return false;
_bufferedDestinations.TryAdd(sequence, destination);
return false;
}
public bool TryGetAcceptedTeleportDestination(
out RuntimeTeleportDestination destination)
{
destination = _acceptedDestination;
return _teleportActive && _hasAcceptedDestination;
}
/// <summary>
/// Read-only preflight used immediately before the host mutates its
/// presentation placement. The post-placement acknowledgement repeats the
/// same exact validation.
/// </summary>
public bool CanPlacePortalDestination(
long generation,
ushort teleportSequence,
uint destinationCell) =>
IsCurrentPortalDestination(
generation,
teleportSequence,
destinationCell);
/// <summary>
/// Ends one graphical/headless portal consumer while preserving
/// duplicate/reorder history for the current network session.
/// </summary>
public void EndTeleport()
{
_teleportActive = false;
_activeTeleportSequence = 0;
_hasPendingTeleportStart = false;
_pendingTeleportStartSequence = 0;
_destinationAccepted = false;
_hasAcceptedDestination = false;
_acceptedDestination = default;
}
public bool AcknowledgeDestinationReadiness(
in RuntimeDestinationReadiness acknowledgement)
{
@ -116,10 +335,24 @@ public sealed class RuntimeWorldTransitState
return true;
}
public bool AcknowledgeMaterialized(
public bool AcknowledgePortalMaterialized(
long generation,
ushort teleportSequence,
uint destinationCell)
{
if (!IsCurrentPortalDestination(
generation,
teleportSequence,
destinationCell))
{
LogRejected(
"materialized-portal-mismatch",
$"generation={generation} "
+ $"sequence={teleportSequence} "
+ $"cell=0x{destinationCell:X8}");
return false;
}
if (!ValidateActive(generation, destinationCell, "materialized"))
return false;
if (_snapshot.Materialized)
@ -247,8 +480,26 @@ public sealed class RuntimeWorldTransitState
public void ResetSession()
{
_snapshot = RuntimePortalSnapshot.Idle;
EndTeleport();
_hasLastTeleportStart = false;
_lastTeleportStartSequence = 0;
_bufferedDestinations.Clear();
}
private bool IsCurrentPortalDestination(
long generation,
ushort teleportSequence,
uint destinationCell) =>
_teleportActive
&& teleportSequence == _activeTeleportSequence
&& generation != 0
&& generation == _snapshot.Generation
&& _snapshot.Kind == RuntimePortalKind.Portal
&& !_snapshot.Cancelled
&& !_snapshot.Completed
&& destinationCell != 0u
&& destinationCell == _snapshot.DestinationCell;
private bool ValidateActive(
long generation,
uint destinationCell,
@ -360,6 +611,21 @@ public sealed class RuntimeWorldTransitState
+ $"failures={snapshot.InvariantFailureCount}");
}
private static bool IsNewer(ushort current, ushort incoming) =>
AcDream.Core.Physics.PhysicsTimestampGate.IsNewer(
current,
incoming);
private void PruneDestinationsOlderThan(ushort sequence)
{
foreach (ushort bufferedSequence
in _bufferedDestinations.Keys.ToArray())
{
if (IsNewer(bufferedSequence, sequence))
_bufferedDestinations.Remove(bufferedSequence);
}
}
private static bool IsIndoor(uint cellId) =>
(cellId & 0xFFFFu) >= 0x0100u;
}

View file

@ -29,7 +29,9 @@ public sealed class WorldSceneRendererTests
{
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u);
RuntimeWorldTransitTestDriver.BeginPortal(
transit,
0x11340021u);
var rig = new Rig(
portalVisible: false,
waitingForLogin: false,

View file

@ -65,7 +65,13 @@ public sealed class CurrentGameRuntimeAdapterTests
};
harness.Objects.AddOrUpdate(item);
harness.Chat.OnSystemMessage("runtime parity", 0x1Au);
harness.WorldReveal.Begin(RuntimePortalKind.Portal, 0x12340001u);
RuntimeWorldTransitTestDriver.AcceptPortalDestination(
harness.WorldTransit,
0x12340001u);
Assert.True(harness.WorldReveal.TryBeginPortal(
1,
0x12340001u,
out _));
WorldRevealReadinessSnapshot readiness =
harness.WorldReveal.PrepareAndEvaluate(0x12340001u);
Assert.True(readiness.IsReady);

View file

@ -22,10 +22,18 @@ public sealed class RuntimeWorldTransitOwnershipTests
appRoot,
"Streaming",
"WorldRevealLifecycleTelemetry.cs")));
Assert.False(File.Exists(Path.Combine(
appRoot,
"Streaming",
"TeleportTransitCoordinator.cs")));
Assert.DoesNotContain(
"class WorldRevealLifecycleTelemetry",
app,
StringComparison.Ordinal);
Assert.DoesNotContain(
"class TeleportTransitCoordinator",
app,
StringComparison.Ordinal);
Assert.DoesNotContain(
"record struct WorldRevealLifecycleSnapshot",
app,
@ -74,6 +82,19 @@ public sealed class RuntimeWorldTransitOwnershipTests
field => field.Name is "_activeGeneration"
or "_worldSimulationReleased"
or "_lifecycle");
var teleportFields = typeof(LocalPlayerTeleportController)
.GetFields(
System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.NonPublic);
Assert.Single(
teleportFields,
field => field.FieldType == typeof(RuntimeWorldTransitState));
Assert.DoesNotContain(
teleportFields,
field => field.Name.Contains(
"acceptedDestination",
StringComparison.OrdinalIgnoreCase));
}
private static string FindRepositoryRoot()

View file

@ -0,0 +1,53 @@
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Runtime;
using AcDream.Runtime.World;
namespace AcDream.App.Tests;
internal static class RuntimeWorldTransitTestDriver
{
public static long BeginPortal(
RuntimeWorldTransitState transit,
uint destinationCell,
ushort sequence = 1)
{
AcceptPortalDestination(transit, destinationCell, sequence);
Assert.True(transit.TryBeginPortalReveal(
sequence,
destinationCell,
out long generation));
return generation;
}
public static void AcceptPortalDestination(
RuntimeWorldTransitState transit,
uint destinationCell,
ushort sequence = 1)
{
Assert.True(transit.TryQueueTeleportStart(sequence));
Assert.True(transit.ActivateQueuedTeleport());
Assert.True(transit.OfferTeleportDestination(
new RuntimeTeleportDestination(
EntityGuid: 0x50000001u,
InstanceSequence: 1,
PositionSequence: 1,
TeleportSequence: sequence,
ForcePositionSequence: 1,
Position: new Position(
destinationCell,
Vector3.Zero,
Quaternion.Identity)),
teleportTimestampAdvanced: true));
}
public static bool MaterializePortal(
RuntimeWorldTransitState transit,
long generation,
uint destinationCell,
ushort sequence = 1) =>
transit.AcknowledgePortalMaterialized(
generation,
sequence,
destinationCell);
}

View file

@ -622,7 +622,7 @@ public sealed class GpuWorldStateVisibilityTests
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
long generation =
transit.BeginReveal(RuntimePortalKind.Portal, cell);
RuntimeWorldTransitTestDriver.BeginPortal(transit, cell);
var state = new GpuWorldState(availability: availability);
state.AddLandblock(new LoadedLandblock(
landblock,
@ -658,7 +658,10 @@ public sealed class GpuWorldStateVisibilityTests
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true)));
Assert.True(transit.AcknowledgeMaterialized(generation, cell));
Assert.True(RuntimeWorldTransitTestDriver.MaterializePortal(
transit,
generation,
cell));
Assert.True(transit.Complete(generation));
Assert.True(state.IsLiveEntityVisible(projectionKey));
Assert.True(record.IsSpatiallyVisible);
@ -705,7 +708,9 @@ public sealed class GpuWorldStateVisibilityTests
{
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
transit.BeginReveal(RuntimePortalKind.Portal, 0x10100001u);
RuntimeWorldTransitTestDriver.BeginPortal(
transit,
0x10100001u);
var centerStatic = Entity(1, 0u);
var centerLive = Entity(2, 0x70000001u);
var neighborStatic = Entity(3, 0u);

View file

@ -38,7 +38,7 @@ public sealed class LocalPlayerTeleportControllerTests
public void DestinationBeforeStart_IsReplayedOnceAfterPresentationActivates()
{
var harness = new Harness();
WorldSession.EntityPositionUpdate destination = Position(
RuntimeTeleportDestination destination = Position(
cellId: 0x20210001u,
teleportSequence: 7,
x: 12f,
@ -231,6 +231,24 @@ public sealed class LocalPlayerTeleportControllerTests
Assert.Equal(1, harness.Reveal.PortalMaterializationCount);
}
[Fact]
public void Place_RechecksExactRuntimeLifetimeBeforeMutatingPresentation()
{
var harness = new Harness();
harness.Controller.OnTeleportStarted(81);
harness.Controller.OfferDestination(
Position(0x20210001u, 81, 4f, 5f, 6f),
teleportTimestampAdvanced: true);
long generation = harness.Reveal.Snapshot.Generation;
Assert.True(harness.Transit.Cancel(generation));
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
harness.Controller.Tick(0.016f);
Assert.Equal(default, harness.Placement.Position);
Assert.Equal(0, harness.Reveal.PortalMaterializationCount);
}
[Fact]
public void LoginComplete_EntersWorldThenSendsThenCompletesAndResets()
{
@ -565,29 +583,21 @@ public sealed class LocalPlayerTeleportControllerTests
return int.MaxValue;
}
private static WorldSession.EntityPositionUpdate Position(
private static RuntimeTeleportDestination Position(
uint cellId,
ushort teleportSequence,
float x,
float y,
float z) => new(
0x50000001u,
new CreateObject.ServerPosition(
EntityGuid: 0x50000001u,
InstanceSequence: 1,
PositionSequence: 1,
TeleportSequence: teleportSequence,
ForcePositionSequence: 1,
Position: new Position(
cellId,
x,
y,
z,
1f,
0f,
0f,
0f),
null,
null,
true,
1,
1,
teleportSequence,
1);
new Vector3(x, y, z),
Quaternion.Identity));
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell) => new(
Guid: guid,
@ -651,6 +661,7 @@ public sealed class LocalPlayerTeleportControllerTests
public readonly FakePlacement Placement;
public readonly FakeSession Session;
public readonly FakePresentation Presentation;
public readonly RuntimeWorldTransitState Transit;
public readonly WorldRevealCoordinator Reveal;
public readonly LocalPlayerTeleportController Controller;
@ -666,8 +677,9 @@ public sealed class LocalPlayerTeleportControllerTests
Placement = new FakePlacement(order);
Session = new FakeSession(order);
Presentation = new FakePresentation(order);
Transit = new RuntimeWorldTransitState(order.Add);
Reveal = new WorldRevealCoordinator(
new RuntimeWorldTransitState(order.Add),
Transit,
isRenderNeighborhoodReady: (_, _) => worldReady,
isSpawnCellReady: _ => worldReady,
isTerrainNeighborhoodReady: (_, _) => worldReady,
@ -681,6 +693,7 @@ public sealed class LocalPlayerTeleportControllerTests
Input,
Mode,
Streaming,
Transit,
Reveal,
Placement,
Session,
@ -830,7 +843,7 @@ public sealed class LocalPlayerTeleportControllerTests
public List<uint> Starts { get; } = [];
public void OnTeleportStarted(uint sequence) => Starts.Add(sequence);
public void OfferDestination(
WorldSession.EntityPositionUpdate update,
RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced)
{
}

View file

@ -0,0 +1,50 @@
using AcDream.App.Streaming;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Runtime;
namespace AcDream.App.Tests.Streaming;
public sealed class RuntimeTeleportDestinationAdapterTests
{
[Fact]
public void AcceptedPosition_PreservesExactIdentityCellFrameAndSequences()
{
var update = new WorldSession.EntityPositionUpdate(
Guid: 0x50000001u,
Position: new CreateObject.ServerPosition(
LandblockId: 0x30310123u,
PositionX: -12.5f,
PositionY: 8.25f,
PositionZ: 91.75f,
RotationW: 0.5f,
RotationX: 0.25f,
RotationY: -0.125f,
RotationZ: 0.75f),
Velocity: new System.Numerics.Vector3(1f, 2f, 3f),
PlacementId: 0x65u,
IsGrounded: false,
InstanceSequence: 0x1234,
PositionSequence: 0x2345,
TeleportSequence: 0x3456,
ForcePositionSequence: 0x4567);
RuntimeTeleportDestination destination =
RuntimeTeleportDestinationAdapter.FromAcceptedPosition(update);
Assert.Equal(update.Guid, destination.EntityGuid);
Assert.Equal(update.InstanceSequence, destination.InstanceSequence);
Assert.Equal(update.PositionSequence, destination.PositionSequence);
Assert.Equal(update.TeleportSequence, destination.TeleportSequence);
Assert.Equal(
update.ForcePositionSequence,
destination.ForcePositionSequence);
Assert.Equal(update.Position.LandblockId, destination.CellId);
Assert.Equal(
new System.Numerics.Vector3(-12.5f, 8.25f, 91.75f),
destination.Position.Frame.Origin);
Assert.Equal(
new System.Numerics.Quaternion(0.25f, -0.125f, 0.75f, 0.5f),
destination.Position.Frame.Orientation);
}
}

View file

@ -1,133 +0,0 @@
using AcDream.App.Streaming;
namespace AcDream.App.Tests.Streaming;
public sealed class TeleportTransitCoordinatorTests
{
[Fact]
public void ReplacementRejectsDelayedPriorDestinationAndAcceptsCurrentAdvance()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.True(transit.QueueStart(10));
Assert.False(transit.Activate(out _));
transit.EndActive();
Assert.True(transit.QueueStart(11));
Assert.False(transit.Activate(out _));
Assert.False(transit.OfferDestination(10, true, 100, out _));
Assert.True(transit.OfferDestination(11, true, 110, out int accepted));
Assert.Equal(110, accepted);
}
[Fact]
public void PositionBeforeF751IsBufferedAndEqualFollowupCannotReplaceIt()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.False(transit.OfferDestination(11, true, 110, out _));
Assert.True(transit.QueueStart(11));
Assert.True(transit.Activate(out int buffered));
Assert.Equal(110, buffered);
Assert.False(transit.OfferDestination(11, false, 111, out _));
}
[Fact]
public void InactiveOrdinaryPositionIsNeverBuffered()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.False(transit.OfferDestination(11, false, 110, out _));
Assert.True(transit.QueueStart(11));
Assert.False(transit.Activate(out _));
}
[Fact]
public void DuplicateStartIsRejectedButSequenceWrapIsFresh()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.True(transit.QueueStart(ushort.MaxValue));
Assert.False(transit.Activate(out _));
transit.EndActive();
Assert.False(transit.CanBegin(ushort.MaxValue));
Assert.False(transit.QueueStart(ushort.MaxValue));
Assert.True(transit.CanBegin(0));
Assert.True(transit.QueueStart(0));
Assert.True(transit.HasPendingStart);
}
[Fact]
public void ExactHalfRangeUsesRetailLowerStampTieBreak()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.True(transit.QueueStart(0x8000));
Assert.False(transit.Activate(out _));
transit.EndActive();
Assert.True(transit.CanBegin(0x0000));
Assert.True(transit.QueueStart(0x0000));
}
[Fact]
public void ClearSessionAllowsSameSequenceInNewSession()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.True(transit.QueueStart(7));
Assert.False(transit.Activate(out _));
transit.ClearSession();
Assert.False(transit.IsActive);
Assert.True(transit.CanBegin(7));
}
[Fact]
public void PendingStartAcceptsEqualPositionUntilPresentationCanActivate()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.True(transit.QueueStart(11));
Assert.False(transit.OfferDestination(11, false, 110, out _));
Assert.True(transit.Activate(out int buffered));
Assert.Equal(110, buffered);
Assert.True(transit.IsActive);
}
[Fact]
public void AdvancingPositionPrunesSkippedDestination()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.False(transit.OfferDestination(11, true, 110, out _));
Assert.False(transit.OfferDestination(12, true, 120, out _));
Assert.True(transit.QueueStart(12));
Assert.True(transit.Activate(out int buffered));
Assert.Equal(120, buffered);
}
[Fact]
public void ObsoleteDestinationCannotSurviveSequenceWrap()
{
var transit = new TeleportTransitCoordinator<int>();
Assert.False(transit.OfferDestination(ushort.MaxValue, true, 999, out _));
Assert.False(transit.OfferDestination(0, true, 0, out _));
Assert.True(transit.QueueStart(0));
Assert.True(transit.Activate(out int wrapped));
Assert.Equal(0, wrapped);
transit.EndActive();
// Advance in legal retail-newer hops until 0xFFFF is current again.
Assert.True(transit.QueueStart(0x7FFF));
Assert.False(transit.Activate(out _));
transit.EndActive();
Assert.True(transit.QueueStart(0xFFFE));
Assert.False(transit.Activate(out _));
transit.EndActive();
Assert.True(transit.QueueStart(ushort.MaxValue));
Assert.False(transit.Activate(out _));
}
}

View file

@ -37,7 +37,9 @@ public sealed class WorldGenerationQuiescenceTests
Assert.Single(world.LandblockBounds);
long generation =
transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell);
RuntimeWorldTransitTestDriver.BeginPortal(
transit,
DestinationCell);
world.CopyLiveEntitiesNearLandblock(LandblockId, 0, nearby);
Assert.False(availability.IsWorldAvailable);
@ -53,7 +55,8 @@ public sealed class WorldGenerationQuiescenceTests
Assert.False(availability.IsWorldAvailable);
Assert.True(transit.AcknowledgeDestinationReadiness(
Ready(generation, DestinationCell)));
Assert.True(transit.AcknowledgeMaterialized(
Assert.True(RuntimeWorldTransitTestDriver.MaterializePortal(
transit,
generation,
DestinationCell));
Assert.True(transit.Complete(generation));
@ -87,12 +90,14 @@ public sealed class WorldGenerationQuiescenceTests
WorldGenerationQuiescenceEdge firstEdge =
quiescence.CaptureBegin();
long first =
transit.BeginReveal(RuntimePortalKind.Login, DestinationCell);
transit.BeginLoginReveal(DestinationCell);
quiescence.CommitBegin(firstEdge);
WorldGenerationQuiescenceEdge secondEdge =
quiescence.CaptureBegin();
long second =
transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell);
RuntimeWorldTransitTestDriver.BeginPortal(
transit,
DestinationCell);
quiescence.CommitBegin(secondEdge);
Assert.False(transit.Complete(first));
@ -104,7 +109,8 @@ public sealed class WorldGenerationQuiescenceTests
Assert.True(transit.AcknowledgeDestinationReadiness(
Ready(second, DestinationCell)));
Assert.True(transit.AcknowledgeMaterialized(
Assert.True(RuntimeWorldTransitTestDriver.MaterializePortal(
transit,
second,
DestinationCell));
Assert.True(transit.Complete(second));
@ -130,7 +136,9 @@ public sealed class WorldGenerationQuiescenceTests
audio: null);
WorldGenerationQuiescenceEdge edge = quiescence.CaptureBegin();
transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell);
RuntimeWorldTransitTestDriver.BeginPortal(
transit,
DestinationCell);
quiescence.CommitBegin(edge);
Assert.Equal(inventoryGuid, selection.SelectedObjectId);

View file

@ -1,5 +1,7 @@
using System.Numerics;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Physics;
using AcDream.Core.Selection;
using AcDream.Runtime;
using AcDream.Runtime.World;
@ -10,6 +12,7 @@ public sealed class WorldRevealCoordinatorTests
{
private sealed class State
{
public RuntimeWorldTransitState Transit { get; private set; } = null!;
public bool RenderReady { get; set; }
public bool CollisionReady { get; set; }
public bool CompositesReady { get; set; }
@ -24,6 +27,7 @@ public sealed class WorldRevealCoordinatorTests
{
transit ??= new RuntimeWorldTransitState(
logs is null ? null : new Action<string>(logs.Add));
Transit = transit;
return new WorldRevealCoordinator(
transit,
isRenderNeighborhoodReady: (_, _) => RenderReady,
@ -48,7 +52,7 @@ public sealed class WorldRevealCoordinatorTests
var state = new State { CompositesReady = true };
WorldRevealCoordinator coordinator = state.Build();
long generation = coordinator.Begin(RuntimePortalKind.Login, 0x11340021u);
long generation = coordinator.BeginLogin(0x11340021u);
Assert.Equal(1, generation);
Assert.Equal(1, state.InvalidateCount);
@ -66,7 +70,7 @@ public sealed class WorldRevealCoordinatorTests
CollisionReady = true,
};
WorldRevealCoordinator coordinator = state.Build();
coordinator.Begin(RuntimePortalKind.Login, outdoorCell);
coordinator.BeginLogin(outdoorCell);
WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell);
state.CompositesReady = true;
@ -91,12 +95,16 @@ public sealed class WorldRevealCoordinatorTests
CompositesReady = true,
};
WorldRevealCoordinator coordinator = state.Build(logs);
coordinator.Begin(RuntimePortalKind.Portal, outdoorCell);
long generation = BeginPortal(
coordinator,
state.Transit,
outdoorCell,
sequence: 1);
state.CompositesReady = true;
Assert.True(coordinator.Evaluate(outdoorCell).IsReady);
coordinator.ObserveMaterialized();
coordinator.ObserveMaterialized();
coordinator.ObserveMaterialized(generation, 1, outdoorCell);
coordinator.ObserveMaterialized(generation, 1, outdoorCell);
coordinator.ObserveWorldViewportVisible();
coordinator.Complete();
@ -108,6 +116,40 @@ public sealed class WorldRevealCoordinatorTests
Assert.Single(logs, line => line.Contains("event=materialized", StringComparison.Ordinal));
}
[Fact]
public void PortalBegin_WrongSequenceOrCellCannotAcquireHostResources()
{
const uint destinationCell = 0x3032001Cu;
var state = new State { CompositesReady = true };
var scheduler = new RecordingDestinationScheduler();
WorldRevealCoordinator coordinator = state.Build(streaming: scheduler);
RuntimeWorldTransitTestDriver.AcceptPortalDestination(
state.Transit,
destinationCell,
sequence: 7);
Assert.False(coordinator.TryBeginPortal(
8,
destinationCell,
out _));
Assert.False(coordinator.TryBeginPortal(
7,
0x11340021u,
out _));
Assert.Empty(scheduler.Begins);
Assert.Equal(0, state.InvalidateCount);
Assert.Equal(0, coordinator.Snapshot.Generation);
Assert.True(coordinator.TryBeginPortal(
7,
destinationCell,
out long generation));
Assert.Equal(
(generation, destinationCell, 1),
Assert.Single(scheduler.Begins));
Assert.Equal(1, state.InvalidateCount);
}
[Fact]
public void Cancel_PreventsLateDestinationWorkFromMutatingLifecycle()
{
@ -118,11 +160,18 @@ public sealed class WorldRevealCoordinatorTests
CompositesReady = true,
};
WorldRevealCoordinator coordinator = state.Build();
coordinator.Begin(RuntimePortalKind.Portal, 0x11340021u);
long generation = BeginPortal(
coordinator,
state.Transit,
0x11340021u,
sequence: 1);
coordinator.Cancel();
coordinator.PrepareAndEvaluate(0x11340021u);
coordinator.ObserveMaterialized();
coordinator.ObserveMaterialized(
generation,
1,
0x11340021u);
coordinator.ObserveWorldViewportVisible();
coordinator.Complete();
@ -155,14 +204,22 @@ public sealed class WorldRevealCoordinatorTests
isSpawnClaimUnhydratable: _ => false,
quiescence: quiescence);
Assert.Equal(1, coordinator.Begin(RuntimePortalKind.Login, 0x11340021u));
Assert.Equal(2, coordinator.Begin(RuntimePortalKind.Portal, 0x11340021u));
Assert.Equal(1, coordinator.BeginLogin(0x11340021u));
long generation = BeginPortal(
coordinator,
transit,
0x11340021u,
sequence: 1);
Assert.Equal(2, generation);
Assert.False(availability.IsWorldAvailable);
Assert.Equal(2, availability.QuiescedGeneration);
Assert.Equal(1, audio.SuspendCalls);
Assert.True(coordinator.Evaluate(0x11340021u).IsReady);
coordinator.ObserveMaterialized();
coordinator.ObserveMaterialized(
generation,
1,
0x11340021u);
coordinator.Complete();
Assert.True(availability.IsWorldAvailable);
@ -193,14 +250,21 @@ public sealed class WorldRevealCoordinatorTests
isSpawnClaimUnhydratable: _ => false,
quiescence: quiescence,
streaming: scheduler);
long generation = coordinator.Begin(
RuntimePortalKind.Portal,
0x3032001Cu);
const uint destinationCell = 0x3032001Cu;
const ushort teleportSequence = 1;
long generation = BeginPortal(
coordinator,
transit,
destinationCell,
teleportSequence);
Assert.False(availability.IsWorldAvailable);
Assert.True(coordinator.Evaluate(0x3032001Cu).IsReady);
Assert.True(coordinator.Evaluate(destinationCell).IsReady);
coordinator.ObserveMaterialized();
coordinator.ObserveMaterialized(
generation,
teleportSequence,
destinationCell);
Assert.True(availability.IsWorldAvailable);
Assert.False(coordinator.Snapshot.WorldViewportObserved);
@ -259,13 +323,21 @@ public sealed class WorldRevealCoordinatorTests
() => calls.Add("spatial")),
availability);
coordinator.Begin(RuntimePortalKind.Portal, 0x3032001Cu);
const uint destinationCell = 0x3032001Cu;
long generation = BeginPortal(
coordinator,
transit,
destinationCell,
sequence: 1);
frame.Tick(1f / 60f);
Assert.Equal(["network", "commands"], calls);
calls.Clear();
Assert.True(coordinator.Evaluate(0x3032001Cu).IsReady);
coordinator.ObserveMaterialized();
Assert.True(coordinator.Evaluate(destinationCell).IsReady);
coordinator.ObserveMaterialized(
generation,
1,
destinationCell);
frame.Tick(1f / 60f);
Assert.Equal(["objects", "network", "commands", "spatial"], calls);
@ -297,9 +369,11 @@ public sealed class WorldRevealCoordinatorTests
isSpawnClaimUnhydratable: _ => false,
quiescence: quiescence,
streaming: scheduler);
long generation = coordinator.Begin(
RuntimePortalKind.Portal,
0x3032001Cu);
long generation = BeginPortal(
coordinator,
transit,
0x3032001Cu,
sequence: 1);
coordinator.ResetSession();
@ -317,12 +391,17 @@ public sealed class WorldRevealCoordinatorTests
var scheduler = new RecordingDestinationScheduler();
WorldRevealCoordinator coordinator = state.Build(streaming: scheduler);
long first = coordinator.Begin(
RuntimePortalKind.Portal,
0x11340021u);
long second = coordinator.Begin(
RuntimePortalKind.Portal,
0x20210123u);
long first = BeginPortal(
coordinator,
state.Transit,
0x11340021u,
sequence: 1);
state.Transit.EndTeleport();
long second = BeginPortal(
coordinator,
state.Transit,
0x20210123u,
sequence: 2);
Assert.Equal(
[
@ -342,12 +421,17 @@ public sealed class WorldRevealCoordinatorTests
WorldRevealCoordinator coordinator =
state.Build(renderResources: resources);
long first = coordinator.Begin(
RuntimePortalKind.Portal,
0x11340021u);
long second = coordinator.Begin(
RuntimePortalKind.Portal,
0x20210123u);
long first = BeginPortal(
coordinator,
state.Transit,
0x11340021u,
sequence: 1);
state.Transit.EndTeleport();
long second = BeginPortal(
coordinator,
state.Transit,
0x20210123u,
sequence: 2);
Assert.Equal([first, second], resources.Begins);
coordinator.RevealWorldViewport();
@ -356,6 +440,33 @@ public sealed class WorldRevealCoordinatorTests
Assert.Equal([second], resources.Ends);
}
private static long BeginPortal(
WorldRevealCoordinator coordinator,
RuntimeWorldTransitState transit,
uint destinationCell,
ushort sequence)
{
Assert.True(transit.TryQueueTeleportStart(sequence));
Assert.True(transit.ActivateQueuedTeleport());
Assert.True(transit.OfferTeleportDestination(
new RuntimeTeleportDestination(
EntityGuid: 0x50000001u,
InstanceSequence: 1,
PositionSequence: 1,
TeleportSequence: sequence,
ForcePositionSequence: 1,
Position: new Position(
destinationCell,
Vector3.Zero,
Quaternion.Identity)),
teleportTimestampAdvanced: true));
Assert.True(coordinator.TryBeginPortal(
sequence,
destinationCell,
out long generation));
return generation;
}
private sealed class RecordingAudioQuiescence : AcDream.App.Audio.IWorldAudioQuiescence
{
public int SuspendCalls { get; private set; }

View file

@ -46,7 +46,9 @@ public sealed class RecallTeleportAnimationTests
var calls = new List<string>();
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u);
RuntimeWorldTransitTestDriver.BeginPortal(
transit,
0x11340021u);
var frame = new RetailLiveFrameCoordinator(
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
new GpuWorldState(availability: availability),
@ -66,7 +68,9 @@ public sealed class RecallTeleportAnimationTests
var calls = new List<string>();
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u);
RuntimeWorldTransitTestDriver.BeginPortal(
transit,
0x11340021u);
var frame = new RetailLiveFrameCoordinator(
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
new GpuWorldState(availability: availability),

View file

@ -142,7 +142,9 @@ public sealed class UpdateFrameOrchestratorTests
var transit = new RuntimeWorldTransitState();
var availability = new WorldGenerationAvailabilityState(transit);
long generation =
transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u);
RuntimeWorldTransitTestDriver.BeginPortal(
transit,
0x11340021u);
UpdateFrameOrchestrator frame = Create(
calls,
observed: observed,
@ -160,7 +162,8 @@ public sealed class UpdateFrameOrchestratorTests
IsRenderNeighborhoodReady: true,
AreCompositeTexturesReady: true,
IsCollisionReady: true)));
Assert.True(transit.AcknowledgeMaterialized(
Assert.True(RuntimeWorldTransitTestDriver.MaterializePortal(
transit,
generation,
0x11340021u));
transit.Complete(generation);
@ -514,9 +517,9 @@ public sealed class UpdateFrameOrchestratorTests
"LocalPlayerTeleportController.cs"));
AssertAppearsInOrder(
teleportSource,
"_transit.CanBegin(teleportSequence)",
"_transit.CanQueueTeleportStart(teleportSequence)",
"_input.EndMouseLook();",
"_transit.QueueStart(teleportSequence)");
"_transit.TryQueueTeleportStart(teleportSequence)");
string playerModeSource = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",

View file

@ -1,3 +1,5 @@
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Runtime.World;
namespace AcDream.Runtime.Tests.World;
@ -13,7 +15,7 @@ public sealed class RuntimeWorldTransitStateTests
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
state.BeginLoginReveal(OutdoorCell);
Assert.Equal(1, generation);
Assert.Equal(generation, state.Snapshot.Generation);
@ -29,7 +31,7 @@ public sealed class RuntimeWorldTransitStateTests
List<string> log = [];
var state = new RuntimeWorldTransitState(log.Add);
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
BeginPortal(state, OutdoorCell);
Assert.False(state.AcknowledgeDestinationReadiness(
Ready(generation + 1, OutdoorCell)));
@ -56,12 +58,13 @@ public sealed class RuntimeWorldTransitStateTests
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
BeginPortal(state, OutdoorCell);
Assert.True(state.AcknowledgeDestinationReadiness(
Ready(generation, OutdoorCell)));
Assert.True(state.AcknowledgeMaterialized(
Assert.True(state.AcknowledgePortalMaterialized(
generation,
1,
OutdoorCell));
Assert.True(state.IsWorldSimulationAvailable);
@ -69,8 +72,9 @@ public sealed class RuntimeWorldTransitStateTests
Assert.False(state.Snapshot.WorldViewportObserved);
Assert.False(state.Snapshot.Completed);
Assert.Equal(1, state.Snapshot.PortalMaterializationCount);
Assert.False(state.AcknowledgeMaterialized(
Assert.False(state.AcknowledgePortalMaterialized(
generation,
1,
OutdoorCell));
Assert.True(state.AcknowledgeWorldViewportVisible(generation));
@ -85,10 +89,11 @@ public sealed class RuntimeWorldTransitStateTests
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
BeginPortal(state, OutdoorCell);
Assert.False(state.AcknowledgeMaterialized(
Assert.False(state.AcknowledgePortalMaterialized(
generation,
1,
OutdoorCell));
Assert.False(state.IsWorldSimulationAvailable);
@ -102,7 +107,7 @@ public sealed class RuntimeWorldTransitStateTests
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
state.BeginLoginReveal(OutdoorCell);
state.AcknowledgeDestinationReadiness(
Ready(generation, OutdoorCell));
@ -120,7 +125,7 @@ public sealed class RuntimeWorldTransitStateTests
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
state.BeginLoginReveal(OutdoorCell);
Assert.False(state.AcknowledgeWorldViewportVisible(generation));
@ -134,7 +139,7 @@ public sealed class RuntimeWorldTransitStateTests
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
state.BeginLoginReveal(OutdoorCell);
RuntimeDestinationReadiness invalid =
Ready(generation, OutdoorCell) with
{
@ -153,7 +158,7 @@ public sealed class RuntimeWorldTransitStateTests
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
BeginPortal(state, OutdoorCell);
RuntimeDestinationReadiness accepted =
Ready(generation, OutdoorCell);
Assert.True(state.AcknowledgeDestinationReadiness(accepted));
@ -174,13 +179,13 @@ public sealed class RuntimeWorldTransitStateTests
{
var state = new RuntimeWorldTransitState();
long login =
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
state.BeginLoginReveal(OutdoorCell);
Assert.False(state.Complete(login));
Assert.False(state.Snapshot.Completed);
long portal =
state.BeginReveal(RuntimePortalKind.Portal, OtherCell);
BeginPortal(state, OtherCell, sequence: 2);
Assert.True(state.AcknowledgeDestinationReadiness(
Ready(portal, OtherCell)));
Assert.False(state.Complete(portal));
@ -193,14 +198,17 @@ public sealed class RuntimeWorldTransitStateTests
{
var state = new RuntimeWorldTransitState();
long first =
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
state.BeginLoginReveal(OutdoorCell);
long second =
state.BeginReveal(RuntimePortalKind.Portal, OtherCell);
BeginPortal(state, OtherCell);
Assert.True(second > first);
Assert.False(state.AcknowledgeDestinationReadiness(
Ready(first, OutdoorCell)));
Assert.False(state.AcknowledgeMaterialized(first, OutdoorCell));
Assert.False(state.AcknowledgePortalMaterialized(
first,
1,
OutdoorCell));
Assert.Equal(second, state.Snapshot.Generation);
Assert.Equal(OtherCell, state.Snapshot.DestinationCell);
@ -214,7 +222,7 @@ public sealed class RuntimeWorldTransitStateTests
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
BeginPortal(state, OutdoorCell);
Assert.True(state.Cancel(generation));
Assert.True(state.IsWorldSimulationAvailable);
@ -222,8 +230,9 @@ public sealed class RuntimeWorldTransitStateTests
Assert.False(state.AcknowledgeDestinationReadiness(
Ready(generation, OutdoorCell)));
Assert.False(state.AcknowledgeMaterialized(
Assert.False(state.AcknowledgePortalMaterialized(
generation,
1,
OutdoorCell));
Assert.False(state.Complete(generation));
Assert.False(state.Snapshot.Materialized);
@ -235,10 +244,13 @@ public sealed class RuntimeWorldTransitStateTests
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
BeginPortal(state, OutdoorCell);
state.AcknowledgeDestinationReadiness(
Ready(generation, OutdoorCell));
state.AcknowledgeMaterialized(generation, OutdoorCell);
state.AcknowledgePortalMaterialized(
generation,
1,
OutdoorCell);
Assert.True(state.Cancel(generation));
Assert.True(state.IsWorldSimulationAvailable);
@ -254,7 +266,7 @@ public sealed class RuntimeWorldTransitStateTests
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
BeginPortal(state, OutdoorCell);
Assert.False(state.ObserveWait(
generation,
@ -278,7 +290,7 @@ public sealed class RuntimeWorldTransitStateTests
{
var state = new RuntimeWorldTransitState();
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
BeginPortal(state, OutdoorCell);
var acknowledgement = new RuntimeDestinationReadiness(
generation,
OutdoorCell,
@ -291,8 +303,9 @@ public sealed class RuntimeWorldTransitStateTests
Assert.True(state.AcknowledgeDestinationReadiness(acknowledgement));
Assert.True(state.Snapshot.IsReady);
Assert.True(state.AcknowledgeMaterialized(
Assert.True(state.AcknowledgePortalMaterialized(
generation,
1,
OutdoorCell));
}
@ -301,14 +314,14 @@ public sealed class RuntimeWorldTransitStateTests
{
var state = new RuntimeWorldTransitState();
long first =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
BeginPortal(state, OutdoorCell);
state.ResetSession();
Assert.Equal(RuntimePortalSnapshot.Idle, state.Snapshot);
Assert.True(state.IsWorldSimulationAvailable);
long second =
state.BeginReveal(RuntimePortalKind.Login, OtherCell);
state.BeginLoginReveal(OtherCell);
Assert.True(second > first);
Assert.False(state.AcknowledgeDestinationReadiness(
Ready(first, OutdoorCell)));
@ -322,12 +335,15 @@ public sealed class RuntimeWorldTransitStateTests
var first = new RuntimeWorldTransitState();
var second = new RuntimeWorldTransitState();
long firstGeneration =
first.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
BeginPortal(first, OutdoorCell);
long secondGeneration =
second.BeginReveal(RuntimePortalKind.Login, OtherCell);
second.BeginLoginReveal(OtherCell);
first.AcknowledgeDestinationReadiness(
Ready(firstGeneration, OutdoorCell));
first.AcknowledgeMaterialized(firstGeneration, OutdoorCell);
first.AcknowledgePortalMaterialized(
firstGeneration,
1,
OutdoorCell);
Assert.Equal(1, first.Snapshot.PortalMaterializationCount);
Assert.Equal(0, second.Snapshot.PortalMaterializationCount);
@ -344,11 +360,12 @@ public sealed class RuntimeWorldTransitStateTests
_ => throw new InvalidOperationException("diagnostic failed"));
long generation =
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
BeginPortal(state, OutdoorCell);
Assert.True(state.AcknowledgeDestinationReadiness(
Ready(generation, OutdoorCell)));
Assert.True(state.AcknowledgeMaterialized(
Assert.True(state.AcknowledgePortalMaterialized(
generation,
1,
OutdoorCell));
Assert.True(state.Complete(generation));
@ -359,6 +376,278 @@ public sealed class RuntimeWorldTransitStateTests
state.LastDiagnosticFailure);
}
[Fact]
public void ReplacementRejectsDelayedPriorDestinationAndAcceptsCurrentAdvance()
{
var state = new RuntimeWorldTransitState();
Assert.True(state.TryQueueTeleportStart(10));
Assert.True(state.ActivateQueuedTeleport());
state.EndTeleport();
Assert.True(state.TryQueueTeleportStart(11));
Assert.True(state.ActivateQueuedTeleport());
Assert.False(state.OfferTeleportDestination(
Destination(OutdoorCell, 10, x: 10f),
teleportTimestampAdvanced: true));
Assert.True(state.OfferTeleportDestination(
Destination(OtherCell, 11, x: 11f),
teleportTimestampAdvanced: true));
Assert.True(state.TryGetAcceptedTeleportDestination(
out RuntimeTeleportDestination accepted));
Assert.Equal(11f, accepted.Position.Frame.Origin.X);
Assert.Equal(OtherCell, accepted.CellId);
}
[Fact]
public void PositionBeforeF751IsBufferedAndEqualFollowupCannotReplaceIt()
{
var state = new RuntimeWorldTransitState();
Assert.False(state.OfferTeleportDestination(
Destination(OutdoorCell, 11, x: 110f),
teleportTimestampAdvanced: true));
Assert.Equal(1, state.BufferedTeleportDestinationCount);
Assert.True(state.TryQueueTeleportStart(11));
Assert.True(state.ActivateQueuedTeleport());
Assert.True(state.TryGetAcceptedTeleportDestination(
out RuntimeTeleportDestination buffered));
Assert.Equal(110f, buffered.Position.Frame.Origin.X);
Assert.False(state.OfferTeleportDestination(
Destination(OtherCell, 11, x: 111f),
teleportTimestampAdvanced: false));
Assert.True(state.TryGetAcceptedTeleportDestination(out buffered));
Assert.Equal(OutdoorCell, buffered.CellId);
}
[Fact]
public void InactiveOrdinaryPositionIsNeverBuffered()
{
var state = new RuntimeWorldTransitState();
Assert.False(state.OfferTeleportDestination(
Destination(OutdoorCell, 11),
teleportTimestampAdvanced: false));
Assert.Equal(0, state.BufferedTeleportDestinationCount);
Assert.True(state.TryQueueTeleportStart(11));
Assert.True(state.ActivateQueuedTeleport());
Assert.False(state.HasAcceptedTeleportDestination);
}
[Fact]
public void DuplicateStartIsRejectedButSequenceWrapIsFresh()
{
var state = new RuntimeWorldTransitState();
Assert.True(state.TryQueueTeleportStart(ushort.MaxValue));
Assert.True(state.ActivateQueuedTeleport());
state.EndTeleport();
Assert.False(state.CanQueueTeleportStart(ushort.MaxValue));
Assert.False(state.TryQueueTeleportStart(ushort.MaxValue));
Assert.True(state.CanQueueTeleportStart(0));
Assert.True(state.TryQueueTeleportStart(0));
Assert.True(state.HasPendingTeleportStart);
}
[Fact]
public void ExactHalfRangeUsesRetailLowerStampTieBreak()
{
var state = new RuntimeWorldTransitState();
Assert.True(state.TryQueueTeleportStart(0x8000));
Assert.True(state.ActivateQueuedTeleport());
state.EndTeleport();
Assert.True(state.CanQueueTeleportStart(0x0000));
Assert.True(state.TryQueueTeleportStart(0x0000));
}
[Fact]
public void PendingStartAcceptsEqualPositionUntilHostCanActivate()
{
var state = new RuntimeWorldTransitState();
Assert.True(state.TryQueueTeleportStart(11));
Assert.False(state.OfferTeleportDestination(
Destination(OutdoorCell, 11, x: 110f),
teleportTimestampAdvanced: false));
Assert.True(state.ActivateQueuedTeleport());
Assert.True(state.TryGetAcceptedTeleportDestination(
out RuntimeTeleportDestination buffered));
Assert.Equal(110f, buffered.Position.Frame.Origin.X);
Assert.True(state.IsTeleportActive);
}
[Fact]
public void AdvancingPositionPrunesSkippedDestination()
{
var state = new RuntimeWorldTransitState();
Assert.False(state.OfferTeleportDestination(
Destination(OutdoorCell, 11),
teleportTimestampAdvanced: true));
Assert.False(state.OfferTeleportDestination(
Destination(OtherCell, 12),
teleportTimestampAdvanced: true));
Assert.Equal(1, state.BufferedTeleportDestinationCount);
Assert.True(state.TryQueueTeleportStart(12));
Assert.True(state.ActivateQueuedTeleport());
Assert.True(state.TryGetAcceptedTeleportDestination(
out RuntimeTeleportDestination buffered));
Assert.Equal(OtherCell, buffered.CellId);
}
[Fact]
public void ObsoleteDestinationCannotSurviveSequenceWrap()
{
var state = new RuntimeWorldTransitState();
Assert.False(state.OfferTeleportDestination(
Destination(OutdoorCell, ushort.MaxValue),
teleportTimestampAdvanced: true));
Assert.False(state.OfferTeleportDestination(
Destination(OtherCell, 0),
teleportTimestampAdvanced: true));
Assert.True(state.TryQueueTeleportStart(0));
Assert.True(state.ActivateQueuedTeleport());
Assert.True(state.TryGetAcceptedTeleportDestination(
out RuntimeTeleportDestination wrapped));
Assert.Equal(OtherCell, wrapped.CellId);
state.EndTeleport();
Assert.True(state.TryQueueTeleportStart(0x7FFF));
Assert.True(state.ActivateQueuedTeleport());
state.EndTeleport();
Assert.True(state.TryQueueTeleportStart(0xFFFE));
Assert.True(state.ActivateQueuedTeleport());
state.EndTeleport();
Assert.True(state.TryQueueTeleportStart(ushort.MaxValue));
Assert.True(state.ActivateQueuedTeleport());
Assert.False(state.HasAcceptedTeleportDestination);
}
[Fact]
public void PortalRevealClaimsOnlyExactAcceptedSequenceAndCell()
{
var state = new RuntimeWorldTransitState();
Assert.True(state.TryQueueTeleportStart(7));
Assert.True(state.ActivateQueuedTeleport());
Assert.True(state.OfferTeleportDestination(
Destination(OutdoorCell, 7),
teleportTimestampAdvanced: true));
Assert.False(state.TryBeginPortalReveal(
8,
OutdoorCell,
out _));
Assert.False(state.TryBeginPortalReveal(
7,
OtherCell,
out _));
Assert.Equal(RuntimePortalSnapshot.Idle.Generation, state.Snapshot.Generation);
Assert.True(state.HasAcceptedTeleportDestination);
Assert.True(state.TryBeginPortalReveal(
7,
OutdoorCell,
out long generation));
Assert.True(generation > 0);
Assert.False(state.HasAcceptedTeleportDestination);
Assert.True(state.CanPlacePortalDestination(
generation,
7,
OutdoorCell));
Assert.False(state.CanPlacePortalDestination(
generation,
8,
OutdoorCell));
Assert.False(state.CanPlacePortalDestination(
generation,
7,
OtherCell));
}
[Fact]
public void MaterializationRequiresExactGenerationSequenceAndCell()
{
var state = new RuntimeWorldTransitState();
long generation = BeginPortal(state, OutdoorCell, sequence: 17);
Assert.True(state.AcknowledgeDestinationReadiness(
Ready(generation, OutdoorCell)));
Assert.False(state.AcknowledgePortalMaterialized(
generation + 1,
17,
OutdoorCell));
Assert.False(state.AcknowledgePortalMaterialized(
generation,
18,
OutdoorCell));
Assert.False(state.AcknowledgePortalMaterialized(
generation,
17,
OtherCell));
Assert.False(state.Snapshot.Materialized);
Assert.True(state.AcknowledgePortalMaterialized(
generation,
17,
OutdoorCell));
Assert.True(state.Snapshot.Materialized);
}
[Fact]
public void ResetSessionClearsCorrelationHistoryAndBufferedDestinations()
{
var state = new RuntimeWorldTransitState();
Assert.False(state.OfferTeleportDestination(
Destination(OutdoorCell, 7),
teleportTimestampAdvanced: true));
Assert.True(state.TryQueueTeleportStart(8));
state.ResetSession();
Assert.False(state.IsTeleportActive);
Assert.False(state.HasPendingTeleportStart);
Assert.False(state.HasAcceptedTeleportDestination);
Assert.Equal(0, state.BufferedTeleportDestinationCount);
Assert.True(state.TryQueueTeleportStart(8));
}
private static long BeginPortal(
RuntimeWorldTransitState state,
uint cell,
ushort sequence = 1)
{
Assert.True(state.TryQueueTeleportStart(sequence));
Assert.True(state.ActivateQueuedTeleport());
Assert.True(state.OfferTeleportDestination(
Destination(cell, sequence),
teleportTimestampAdvanced: true));
Assert.True(state.TryBeginPortalReveal(
sequence,
cell,
out long generation));
return generation;
}
private static RuntimeTeleportDestination Destination(
uint cell,
ushort sequence,
float x = 1f) =>
new(
EntityGuid: 0x50000001u,
InstanceSequence: 1,
PositionSequence: 1,
TeleportSequence: sequence,
ForcePositionSequence: 1,
Position: new Position(
cell,
new Vector3(x, 2f, 3f),
Quaternion.Identity));
private static RuntimeDestinationReadiness Ready(
long generation,
uint cell) =>