refactor(runtime): own teleport destination correlation
This commit is contained in:
parent
38b3773cb9
commit
6a063a27d4
23 changed files with 1209 additions and 487 deletions
|
|
@ -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
|
consumed even if the channel already advanced. Thus reordered delivery and
|
||||||
retransmitted notifications cannot materialize the wrong destination or
|
retransmitted notifications cannot materialize the wrong destination or
|
||||||
strand the transit, while canonical physics retains its own retail gates.
|
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
|
- Destination placement resets the retail chase camera's published and sought
|
||||||
positions to the player before the normal update path resumes.
|
positions to the player before the normal update path resumes.
|
||||||
- `RetailLiveFrameCoordinator` owns the `SmartBox::UseTime` phase barrier:
|
- `RetailLiveFrameCoordinator` owns the `SmartBox::UseTime` phase barrier:
|
||||||
|
|
|
||||||
|
|
@ -741,6 +741,7 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
streamingOriginRecenter,
|
streamingOriginRecenter,
|
||||||
streaming,
|
streaming,
|
||||||
sealedDungeonCells),
|
sealedDungeonCells),
|
||||||
|
live.WorldTransit,
|
||||||
worldReveal,
|
worldReveal,
|
||||||
new LocalPlayerTeleportPlacement(
|
new LocalPlayerTeleportPlacement(
|
||||||
d.PhysicsEngine,
|
d.PhysicsEngine,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using AcDream.App.Net;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.Update;
|
using AcDream.App.Update;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Content;
|
using AcDream.Content;
|
||||||
|
|
@ -1764,7 +1765,8 @@ internal sealed class LiveEntityNetworkUpdateController
|
||||||
&& update.Guid == _playerServerGuid)
|
&& update.Guid == _playerServerGuid)
|
||||||
{
|
{
|
||||||
_localPlayerTeleport.OfferDestination(
|
_localPlayerTeleport.OfferDestination(
|
||||||
update,
|
RuntimeTeleportDestinationAdapter.FromAcceptedPosition(
|
||||||
|
update),
|
||||||
timestamps.TeleportAdvanced);
|
timestamps.TeleportAdvanced);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Update;
|
using AcDream.App.Update;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.Net;
|
|
||||||
using AcDream.Core.Net.Messages;
|
using AcDream.Core.Net.Messages;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Rendering;
|
using AcDream.Core.Rendering;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
|
using AcDream.Runtime.World;
|
||||||
|
|
||||||
namespace AcDream.App.Streaming;
|
namespace AcDream.App.Streaming;
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ internal interface ILocalPlayerTeleportNetworkSink
|
||||||
void OnTeleportStarted(uint sequence);
|
void OnTeleportStarted(uint sequence);
|
||||||
|
|
||||||
void OfferDestination(
|
void OfferDestination(
|
||||||
WorldSession.EntityPositionUpdate update,
|
RuntimeTeleportDestination destination,
|
||||||
bool teleportTimestampAdvanced);
|
bool teleportTimestampAdvanced);
|
||||||
|
|
||||||
void ResetSession();
|
void ResetSession();
|
||||||
|
|
@ -56,9 +56,9 @@ internal sealed class DeferredLocalPlayerTeleportNetworkSink
|
||||||
public void OnTeleportStarted(uint sequence) => Required().OnTeleportStarted(sequence);
|
public void OnTeleportStarted(uint sequence) => Required().OnTeleportStarted(sequence);
|
||||||
|
|
||||||
public void OfferDestination(
|
public void OfferDestination(
|
||||||
WorldSession.EntityPositionUpdate update,
|
RuntimeTeleportDestination destination,
|
||||||
bool teleportTimestampAdvanced) =>
|
bool teleportTimestampAdvanced) =>
|
||||||
Required().OfferDestination(update, teleportTimestampAdvanced);
|
Required().OfferDestination(destination, teleportTimestampAdvanced);
|
||||||
|
|
||||||
public void ResetSession() => Required().ResetSession();
|
public void ResetSession() => Required().ResetSession();
|
||||||
|
|
||||||
|
|
@ -372,9 +372,10 @@ internal sealed class LocalPlayerTeleportPresentation
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Single update-thread owner of the local F751/Position-correlated portal
|
/// Graphical-host orchestrator for Runtime's local F751/Position-correlated
|
||||||
/// lifetime: activation, destination aim, readiness hold, Place, viewport
|
/// portal lifetime. Runtime owns sequence/destination/reveal authority; this
|
||||||
/// transitions, LoginComplete, and session/reset symmetry.
|
/// update-thread adapter owns portal viewport activation, render-space aim,
|
||||||
|
/// readiness preparation, placement callbacks, and LoginComplete presentation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class LocalPlayerTeleportController
|
internal sealed class LocalPlayerTeleportController
|
||||||
: ILocalPlayerTeleportFramePhase,
|
: ILocalPlayerTeleportFramePhase,
|
||||||
|
|
@ -382,7 +383,7 @@ internal sealed class LocalPlayerTeleportController
|
||||||
AcDream.App.Interaction.ISelectionViewPlaneSource,
|
AcDream.App.Interaction.ISelectionViewPlaneSource,
|
||||||
IDisposable
|
IDisposable
|
||||||
{
|
{
|
||||||
private readonly TeleportTransitCoordinator<WorldSession.EntityPositionUpdate> _transit = new();
|
private readonly RuntimeWorldTransitState _transit;
|
||||||
private readonly ILocalPlayerTeleportAuthority _authority;
|
private readonly ILocalPlayerTeleportAuthority _authority;
|
||||||
private readonly ILocalPlayerTeleportInputLifetime _input;
|
private readonly ILocalPlayerTeleportInputLifetime _input;
|
||||||
private readonly ILocalPlayerTeleportModeOperations _mode;
|
private readonly ILocalPlayerTeleportModeOperations _mode;
|
||||||
|
|
@ -395,8 +396,7 @@ internal sealed class LocalPlayerTeleportController
|
||||||
private Vector3 _pendingPosition;
|
private Vector3 _pendingPosition;
|
||||||
private uint _pendingCell;
|
private uint _pendingCell;
|
||||||
private Quaternion _pendingRotation = Quaternion.Identity;
|
private Quaternion _pendingRotation = Quaternion.Identity;
|
||||||
private bool _hasAcceptedDestination;
|
private long _pendingRevealGeneration;
|
||||||
private WorldSession.EntityPositionUpdate _acceptedDestination;
|
|
||||||
private float _holdSeconds;
|
private float _holdSeconds;
|
||||||
private long _lifetimeGeneration;
|
private long _lifetimeGeneration;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
@ -406,6 +406,7 @@ internal sealed class LocalPlayerTeleportController
|
||||||
ILocalPlayerTeleportInputLifetime input,
|
ILocalPlayerTeleportInputLifetime input,
|
||||||
ILocalPlayerTeleportModeOperations mode,
|
ILocalPlayerTeleportModeOperations mode,
|
||||||
ILocalPlayerTeleportStreamingOperations streaming,
|
ILocalPlayerTeleportStreamingOperations streaming,
|
||||||
|
RuntimeWorldTransitState transit,
|
||||||
WorldRevealCoordinator worldReveal,
|
WorldRevealCoordinator worldReveal,
|
||||||
ILocalPlayerTeleportPlacement placement,
|
ILocalPlayerTeleportPlacement placement,
|
||||||
ILocalPlayerTeleportSession session,
|
ILocalPlayerTeleportSession session,
|
||||||
|
|
@ -415,22 +416,24 @@ internal sealed class LocalPlayerTeleportController
|
||||||
_input = input ?? throw new ArgumentNullException(nameof(input));
|
_input = input ?? throw new ArgumentNullException(nameof(input));
|
||||||
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
|
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
|
||||||
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
|
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
|
||||||
|
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
|
||||||
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
|
_worldReveal = worldReveal ?? throw new ArgumentNullException(nameof(worldReveal));
|
||||||
_placement = placement ?? throw new ArgumentNullException(nameof(placement));
|
_placement = placement ?? throw new ArgumentNullException(nameof(placement));
|
||||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||||
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
|
_presentation = presentation ?? throw new ArgumentNullException(nameof(presentation));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsActive => _transit.IsActive;
|
public bool IsActive => _transit.IsTeleportActive;
|
||||||
public bool IsPortalViewportVisible => _presentation.IsPortalViewportVisible;
|
public bool IsPortalViewportVisible => _presentation.IsPortalViewportVisible;
|
||||||
public uint ActiveDestinationCell => _transit.IsActive ? _pendingCell : 0u;
|
public uint ActiveDestinationCell =>
|
||||||
|
_transit.IsTeleportActive ? _pendingCell : 0u;
|
||||||
|
|
||||||
public void OnTeleportStarted(uint sequence)
|
public void OnTeleportStarted(uint sequence)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
ushort teleportSequence = (ushort)sequence;
|
ushort teleportSequence = (ushort)sequence;
|
||||||
if (!_authority.IsFreshStart(teleportSequence)
|
if (!_authority.IsFreshStart(teleportSequence)
|
||||||
|| !_transit.CanBegin(teleportSequence))
|
|| !_transit.CanQueueTeleportStart(teleportSequence))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -444,24 +447,21 @@ internal sealed class LocalPlayerTeleportController
|
||||||
if (_lifetimeGeneration != resetGeneration)
|
if (_lifetimeGeneration != resetGeneration)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_transit.QueueStart(teleportSequence);
|
if (!_transit.TryQueueTeleportStart(teleportSequence))
|
||||||
|
return;
|
||||||
TryActivatePendingPresentation();
|
TryActivatePendingPresentation();
|
||||||
Console.WriteLine($"live: teleport queued (seq={sequence})");
|
Console.WriteLine($"live: teleport queued (seq={sequence})");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OfferDestination(
|
public void OfferDestination(
|
||||||
WorldSession.EntityPositionUpdate update,
|
RuntimeTeleportDestination destination,
|
||||||
bool teleportTimestampAdvanced)
|
bool teleportTimestampAdvanced)
|
||||||
{
|
{
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
if (_transit.OfferDestination(
|
_transit.OfferTeleportDestination(
|
||||||
update.TeleportSequence,
|
destination,
|
||||||
teleportTimestampAdvanced,
|
teleportTimestampAdvanced);
|
||||||
update,
|
TryAimAcceptedDestination();
|
||||||
out WorldSession.EntityPositionUpdate destination))
|
|
||||||
{
|
|
||||||
AcceptDestination(destination);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Tick(float deltaSeconds)
|
public void Tick(float deltaSeconds)
|
||||||
|
|
@ -469,11 +469,11 @@ internal sealed class LocalPlayerTeleportController
|
||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
TryActivatePendingPresentation();
|
TryActivatePendingPresentation();
|
||||||
TryAimAcceptedDestination();
|
TryAimAcceptedDestination();
|
||||||
if (!_transit.IsActive)
|
if (!_transit.IsTeleportActive)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
long generation = _lifetimeGeneration;
|
long generation = _lifetimeGeneration;
|
||||||
ushort sequence = _transit.Sequence;
|
ushort sequence = _transit.ActiveTeleportSequence;
|
||||||
if (!_mode.TryEnterPortalSpace()
|
if (!_mode.TryEnterPortalSpace()
|
||||||
|| !IsCurrentLifetime(generation, sequence)
|
|| !IsCurrentLifetime(generation, sequence)
|
||||||
|| _mode.Controller is null)
|
|| _mode.Controller is null)
|
||||||
|
|
@ -506,13 +506,23 @@ internal sealed class LocalPlayerTeleportController
|
||||||
switch (teleportEvent)
|
switch (teleportEvent)
|
||||||
{
|
{
|
||||||
case TeleportAnimEvent.Place:
|
case TeleportAnimEvent.Place:
|
||||||
|
if (!_worldReveal.CanPlacePortalDestination(
|
||||||
|
_pendingRevealGeneration,
|
||||||
|
sequence,
|
||||||
|
_pendingCell))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
_placement.Place(
|
_placement.Place(
|
||||||
_pendingPosition,
|
_pendingPosition,
|
||||||
_pendingCell,
|
_pendingCell,
|
||||||
_pendingRotation);
|
_pendingRotation);
|
||||||
if (!IsCurrentLifetime(generation, sequence))
|
if (!IsCurrentLifetime(generation, sequence))
|
||||||
return;
|
return;
|
||||||
_worldReveal.ObserveMaterialized();
|
_worldReveal.ObserveMaterialized(
|
||||||
|
_pendingRevealGeneration,
|
||||||
|
sequence,
|
||||||
|
_pendingCell);
|
||||||
if (!IsCurrentLifetime(generation, sequence))
|
if (!IsCurrentLifetime(generation, sequence))
|
||||||
return;
|
return;
|
||||||
break;
|
break;
|
||||||
|
|
@ -571,43 +581,43 @@ internal sealed class LocalPlayerTeleportController
|
||||||
|
|
||||||
private void TryActivatePendingPresentation()
|
private void TryActivatePendingPresentation()
|
||||||
{
|
{
|
||||||
if (!_transit.HasPendingStart)
|
if (!_transit.HasPendingTeleportStart)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
long generation = _lifetimeGeneration;
|
long generation = _lifetimeGeneration;
|
||||||
if (!_mode.TryEnterPortalSpace()
|
if (!_mode.TryEnterPortalSpace()
|
||||||
|| _lifetimeGeneration != generation
|
|| _lifetimeGeneration != generation
|
||||||
|| !_transit.HasPendingStart)
|
|| !_transit.HasPendingTeleportStart)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_holdSeconds = 0f;
|
_holdSeconds = 0f;
|
||||||
_presentation.Begin(_mode.Projection);
|
_presentation.Begin(_mode.Projection);
|
||||||
if (_lifetimeGeneration != generation || !_transit.HasPendingStart)
|
if (_lifetimeGeneration != generation
|
||||||
|
|| !_transit.HasPendingTeleportStart)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
bool hasBufferedDestination = _transit.Activate(out var destination);
|
if (!_transit.ActivateQueuedTeleport())
|
||||||
if (hasBufferedDestination)
|
return;
|
||||||
AcceptDestination(destination);
|
|
||||||
Console.WriteLine(
|
|
||||||
$"live: teleport presentation started (seq={_transit.Sequence})");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AcceptDestination(WorldSession.EntityPositionUpdate destination)
|
|
||||||
{
|
|
||||||
_acceptedDestination = destination;
|
|
||||||
_hasAcceptedDestination = true;
|
|
||||||
TryAimAcceptedDestination();
|
TryAimAcceptedDestination();
|
||||||
|
Console.WriteLine(
|
||||||
|
$"live: teleport presentation started "
|
||||||
|
+ $"(seq={_transit.ActiveTeleportSequence})");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TryAimAcceptedDestination()
|
private void TryAimAcceptedDestination()
|
||||||
{
|
{
|
||||||
if (!_hasAcceptedDestination || !_transit.IsActive)
|
if (!_transit.TryGetAcceptedTeleportDestination(
|
||||||
|
out RuntimeTeleportDestination destination))
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
long generation = _lifetimeGeneration;
|
long generation = _lifetimeGeneration;
|
||||||
ushort sequence = _transit.Sequence;
|
ushort sequence = _transit.ActiveTeleportSequence;
|
||||||
PlayerMovementController? controller = _mode.Controller;
|
PlayerMovementController? controller = _mode.Controller;
|
||||||
if (controller is null)
|
if (controller is null)
|
||||||
{
|
{
|
||||||
|
|
@ -622,26 +632,19 @@ internal sealed class LocalPlayerTeleportController
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
WorldSession.EntityPositionUpdate destination = _acceptedDestination;
|
|
||||||
if (!AimDestination(destination, controller, generation, sequence))
|
if (!AimDestination(destination, controller, generation, sequence))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (IsCurrentLifetime(generation, sequence))
|
|
||||||
{
|
|
||||||
_hasAcceptedDestination = false;
|
|
||||||
_acceptedDestination = default;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool AimDestination(
|
private bool AimDestination(
|
||||||
WorldSession.EntityPositionUpdate update,
|
RuntimeTeleportDestination destination,
|
||||||
PlayerMovementController controller,
|
PlayerMovementController controller,
|
||||||
long generation,
|
long generation,
|
||||||
ushort sequence)
|
ushort sequence)
|
||||||
{
|
{
|
||||||
var position = update.Position;
|
Position position = destination.Position;
|
||||||
int landblockX = (int)((position.LandblockId >> 24) & 0xFFu);
|
int landblockX = (int)((position.ObjCellId >> 24) & 0xFFu);
|
||||||
int landblockY = (int)((position.LandblockId >> 16) & 0xFFu);
|
int landblockY = (int)((position.ObjCellId >> 16) & 0xFFu);
|
||||||
uint streamingOriginLandblockId = StreamingRegion.EncodeLandblockId(
|
uint streamingOriginLandblockId = StreamingRegion.EncodeLandblockId(
|
||||||
_streaming.CenterX,
|
_streaming.CenterX,
|
||||||
_streaming.CenterY);
|
_streaming.CenterY);
|
||||||
|
|
@ -649,14 +652,11 @@ internal sealed class LocalPlayerTeleportController
|
||||||
(landblockX - _streaming.CenterX) * 192f,
|
(landblockX - _streaming.CenterX) * 192f,
|
||||||
(landblockY - _streaming.CenterY) * 192f,
|
(landblockY - _streaming.CenterY) * 192f,
|
||||||
0f);
|
0f);
|
||||||
var translated = new Vector3(
|
Vector3 translated = position.Frame.Origin + origin;
|
||||||
position.PositionX,
|
|
||||||
position.PositionY,
|
|
||||||
position.PositionZ) + origin;
|
|
||||||
|
|
||||||
TeleportLandblockTransition transition = TeleportLandblockTransition.Classify(
|
TeleportLandblockTransition transition = TeleportLandblockTransition.Classify(
|
||||||
controller.CellId,
|
controller.CellId,
|
||||||
position.LandblockId,
|
position.ObjCellId,
|
||||||
streamingOriginLandblockId);
|
streamingOriginLandblockId);
|
||||||
int oldX = (int)((transition.SourceLandblockId >> 24) & 0xFFu);
|
int oldX = (int)((transition.SourceLandblockId >> 24) & 0xFFu);
|
||||||
int oldY = (int)((transition.SourceLandblockId >> 16) & 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,
|
// Retail SmartBox enters blocking_for_cells before old-world object,
|
||||||
// physics, landscape, and ambient owners may advance again. Begin the
|
// physics, landscape, and ambient owners may advance again. Begin the
|
||||||
// reveal generation before a recenter can start detaching that world.
|
// 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))
|
if (!IsCurrentLifetime(generation, sequence))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
@ -677,7 +684,7 @@ internal sealed class LocalPlayerTeleportController
|
||||||
if (transition.ChangesStreamingCenter)
|
if (transition.ChangesStreamingCenter)
|
||||||
{
|
{
|
||||||
bool isSealedDungeon = _streaming.IsSealedDungeon(
|
bool isSealedDungeon = _streaming.IsSealedDungeon(
|
||||||
position.LandblockId);
|
position.ObjCellId);
|
||||||
if (!IsCurrentLifetime(generation, sequence))
|
if (!IsCurrentLifetime(generation, sequence))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|
@ -688,28 +695,24 @@ internal sealed class LocalPlayerTeleportController
|
||||||
if (!IsCurrentLifetime(generation, sequence))
|
if (!IsCurrentLifetime(generation, sequence))
|
||||||
return false;
|
return false;
|
||||||
worldPosition = new Vector3(
|
worldPosition = new Vector3(
|
||||||
position.PositionX,
|
position.Frame.Origin.X,
|
||||||
position.PositionY,
|
position.Frame.Origin.Y,
|
||||||
position.PositionZ);
|
position.Frame.Origin.Z);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
worldPosition = translated;
|
worldPosition = translated;
|
||||||
}
|
}
|
||||||
|
|
||||||
_pendingRotation = new Quaternion(
|
_pendingRotation = position.Frame.Orientation;
|
||||||
position.RotationX,
|
|
||||||
position.RotationY,
|
|
||||||
position.RotationZ,
|
|
||||||
position.RotationW);
|
|
||||||
_pendingPosition = worldPosition;
|
_pendingPosition = worldPosition;
|
||||||
_pendingCell = position.LandblockId;
|
_pendingCell = position.ObjCellId;
|
||||||
_holdSeconds = 0f;
|
_holdSeconds = 0f;
|
||||||
PhysicsDiagnostics.LogTeleport(
|
PhysicsDiagnostics.LogTeleport(
|
||||||
"AIM",
|
"AIM",
|
||||||
position.LandblockId,
|
position.ObjCellId,
|
||||||
$"seq={update.TeleportSequence} lb={landblockX},{landblockY} "
|
$"seq={destination.TeleportSequence} lb={landblockX},{landblockY} "
|
||||||
+ $"indoor={((position.LandblockId & 0xFFFFu) >= 0x0100u)} "
|
+ $"indoor={((position.ObjCellId & 0xFFFFu) >= 0x0100u)} "
|
||||||
+ $"playerCross={transition.CrossesLandblock} "
|
+ $"playerCross={transition.CrossesLandblock} "
|
||||||
+ $"centerChange={transition.ChangesStreamingCenter}");
|
+ $"centerChange={transition.ChangesStreamingCenter}");
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -718,20 +721,11 @@ internal sealed class LocalPlayerTeleportController
|
||||||
private long ResetTransit(bool clearSession)
|
private long ResetTransit(bool clearSession)
|
||||||
{
|
{
|
||||||
long generation = checked(++_lifetimeGeneration);
|
long generation = checked(++_lifetimeGeneration);
|
||||||
if (clearSession)
|
|
||||||
{
|
|
||||||
_transit.ClearSession();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_transit.EndActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
_pendingPosition = default;
|
_pendingPosition = default;
|
||||||
_pendingCell = 0u;
|
_pendingCell = 0u;
|
||||||
_pendingRotation = Quaternion.Identity;
|
_pendingRotation = Quaternion.Identity;
|
||||||
_hasAcceptedDestination = false;
|
_pendingRevealGeneration = 0;
|
||||||
_acceptedDestination = default;
|
|
||||||
_holdSeconds = 0f;
|
_holdSeconds = 0f;
|
||||||
|
|
||||||
_streaming.ResetRecenter(clearSession);
|
_streaming.ResetRecenter(clearSession);
|
||||||
|
|
@ -741,7 +735,10 @@ internal sealed class LocalPlayerTeleportController
|
||||||
if (clearSession)
|
if (clearSession)
|
||||||
_worldReveal.ResetSession();
|
_worldReveal.ResetSession();
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
_transit.EndTeleport();
|
||||||
_worldReveal.Cancel();
|
_worldReveal.Cancel();
|
||||||
|
}
|
||||||
if (_lifetimeGeneration != generation)
|
if (_lifetimeGeneration != generation)
|
||||||
return generation;
|
return generation;
|
||||||
|
|
||||||
|
|
@ -754,8 +751,8 @@ internal sealed class LocalPlayerTeleportController
|
||||||
|
|
||||||
private bool IsCurrentLifetime(long generation, ushort sequence) =>
|
private bool IsCurrentLifetime(long generation, ushort sequence) =>
|
||||||
_lifetimeGeneration == generation
|
_lifetimeGeneration == generation
|
||||||
&& _transit.IsActive
|
&& _transit.IsTeleportActive
|
||||||
&& _transit.Sequence == sequence;
|
&& _transit.ActiveTeleportSequence == sequence;
|
||||||
|
|
||||||
private void ThrowIfDisposed() =>
|
private void ThrowIfDisposed() =>
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
|
||||||
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -77,7 +77,7 @@ internal sealed class WorldRevealCoordinator
|
||||||
_transit.Snapshot.PortalMaterializationCount;
|
_transit.Snapshot.PortalMaterializationCount;
|
||||||
public bool WaitCueShown => _transit.Snapshot.WaitCueShown;
|
public bool WaitCueShown => _transit.Snapshot.WaitCueShown;
|
||||||
|
|
||||||
public long Begin(RuntimePortalKind kind, uint destinationCell)
|
public long BeginLogin(uint destinationCell)
|
||||||
{
|
{
|
||||||
if (destinationCell == 0u)
|
if (destinationCell == 0u)
|
||||||
throw new ArgumentOutOfRangeException(nameof(destinationCell));
|
throw new ArgumentOutOfRangeException(nameof(destinationCell));
|
||||||
|
|
@ -85,7 +85,45 @@ internal sealed class WorldRevealCoordinator
|
||||||
WorldGenerationQuiescenceEdge quiescenceEdge =
|
WorldGenerationQuiescenceEdge quiescenceEdge =
|
||||||
_quiescence?.CaptureBegin() ?? default;
|
_quiescence?.CaptureBegin() ?? default;
|
||||||
_readiness.Begin();
|
_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);
|
_quiescence?.CommitBegin(quiescenceEdge);
|
||||||
_reservationGeneration = generation;
|
_reservationGeneration = generation;
|
||||||
_worldViewportReleased = false;
|
_worldViewportReleased = false;
|
||||||
|
|
@ -94,7 +132,6 @@ internal sealed class WorldRevealCoordinator
|
||||||
destinationCell,
|
destinationCell,
|
||||||
WorldRevealReadinessBarrier.RequiredRenderRadius(destinationCell));
|
WorldRevealReadinessBarrier.RequiredRenderRadius(destinationCell));
|
||||||
_renderResources?.BeginDestinationReveal(generation);
|
_renderResources?.BeginDestinationReveal(generation);
|
||||||
return generation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -138,17 +175,27 @@ internal sealed class WorldRevealCoordinator
|
||||||
/// <c>TAS_TUNNEL_CONTINUE</c>. The normal world viewport is not restored
|
/// <c>TAS_TUNNEL_CONTINUE</c>. The normal world viewport is not restored
|
||||||
/// until the later <c>TUNNEL_FADE_OUT -> WORLD_FADE_IN</c> edge.
|
/// until the later <c>TUNNEL_FADE_OUT -> WORLD_FADE_IN</c> edge.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public void ObserveMaterialized()
|
public bool CanPlacePortalDestination(
|
||||||
{
|
long generation,
|
||||||
RuntimePortalSnapshot snapshot = _transit.Snapshot;
|
ushort teleportSequence,
|
||||||
if (snapshot.Generation == 0)
|
uint destinationCell) =>
|
||||||
return;
|
_transit.CanPlacePortalDestination(
|
||||||
|
generation,
|
||||||
|
teleportSequence,
|
||||||
|
destinationCell);
|
||||||
|
|
||||||
|
public bool ObserveMaterialized(
|
||||||
|
long generation,
|
||||||
|
ushort teleportSequence,
|
||||||
|
uint destinationCell)
|
||||||
|
{
|
||||||
bool wasAvailable = _transit.IsWorldSimulationAvailable;
|
bool wasAvailable = _transit.IsWorldSimulationAvailable;
|
||||||
_transit.AcknowledgeMaterialized(
|
bool acknowledged = _transit.AcknowledgePortalMaterialized(
|
||||||
snapshot.Generation,
|
generation,
|
||||||
snapshot.DestinationCell);
|
teleportSequence,
|
||||||
|
destinationCell);
|
||||||
ObserveSimulationRelease(wasAvailable);
|
ObserveSimulationRelease(wasAvailable);
|
||||||
|
return acknowledged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ObserveWorldViewportVisible()
|
public void ObserveWorldViewportVisible()
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@ internal sealed class LiveEntityWorldOriginCoordinator : ILiveEntityWorldOriginC
|
||||||
lbY,
|
lbY,
|
||||||
isSealedDungeon: _sealedDungeonCells.IsSealedDungeon(
|
isSealedDungeon: _sealedDungeonCells.IsSealedDungeon(
|
||||||
position.LandblockId));
|
position.LandblockId));
|
||||||
_worldReveal.Begin(RuntimePortalKind.Login, position.LandblockId);
|
_worldReveal.BeginLogin(position.LandblockId);
|
||||||
|
|
||||||
return new(
|
return new(
|
||||||
true,
|
true,
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,23 @@ public enum RuntimePortalKind
|
||||||
Portal,
|
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(
|
public readonly record struct RuntimeDestinationReadiness(
|
||||||
long Generation,
|
long Generation,
|
||||||
uint DestinationCell,
|
uint DestinationCell,
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,16 @@ using System.Globalization;
|
||||||
namespace AcDream.Runtime.World;
|
namespace AcDream.Runtime.World;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Canonical presentation-independent owner of one login/portal reveal
|
/// Canonical presentation-independent owner of local F751/Position
|
||||||
/// generation. The graphical or headless host supplies typed destination
|
/// correlation and one login/portal reveal generation. The graphical or
|
||||||
/// readiness acknowledgements; Runtime owns which generation and destination
|
/// headless host supplies typed destination readiness acknowledgements;
|
||||||
/// those acknowledgements are allowed to advance.
|
/// Runtime owns which teleport sequence, generation, and destination those
|
||||||
|
/// acknowledgements are allowed to advance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <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>SmartBox::UseTime @ 0x00455410</c>, and
|
||||||
/// <c>gmSmartBoxUI::EndTeleportAnimation @ 0x004D65A0</c>. Destination
|
/// <c>gmSmartBoxUI::EndTeleportAnimation @ 0x004D65A0</c>. Destination
|
||||||
/// materialization resumes simulation before the later normal-world viewport
|
/// materialization resumes simulation before the later normal-world viewport
|
||||||
|
|
@ -22,8 +25,19 @@ public sealed class RuntimeWorldTransitState
|
||||||
TimeSpan.FromSeconds(5);
|
TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
private readonly Action<string> _log;
|
private readonly Action<string> _log;
|
||||||
|
private readonly Dictionary<ushort, RuntimeTeleportDestination>
|
||||||
|
_bufferedDestinations = [];
|
||||||
private RuntimePortalSnapshot _snapshot = RuntimePortalSnapshot.Idle;
|
private RuntimePortalSnapshot _snapshot = RuntimePortalSnapshot.Idle;
|
||||||
private long _nextGeneration;
|
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)
|
public RuntimeWorldTransitState(Action<string>? log = null)
|
||||||
{
|
{
|
||||||
|
|
@ -33,10 +47,70 @@ public sealed class RuntimeWorldTransitState
|
||||||
public RuntimePortalSnapshot Snapshot => _snapshot;
|
public RuntimePortalSnapshot Snapshot => _snapshot;
|
||||||
public bool IsWorldSimulationAvailable =>
|
public bool IsWorldSimulationAvailable =>
|
||||||
_snapshot.WorldSimulationAvailable;
|
_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 int DiagnosticFailureCount { get; private set; }
|
||||||
public Exception? LastDiagnosticFailure { 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,
|
RuntimePortalKind kind,
|
||||||
uint destinationCell)
|
uint destinationCell)
|
||||||
{
|
{
|
||||||
|
|
@ -79,6 +153,151 @@ public sealed class RuntimeWorldTransitState
|
||||||
return generation;
|
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(
|
public bool AcknowledgeDestinationReadiness(
|
||||||
in RuntimeDestinationReadiness acknowledgement)
|
in RuntimeDestinationReadiness acknowledgement)
|
||||||
{
|
{
|
||||||
|
|
@ -116,10 +335,24 @@ public sealed class RuntimeWorldTransitState
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AcknowledgeMaterialized(
|
public bool AcknowledgePortalMaterialized(
|
||||||
long generation,
|
long generation,
|
||||||
|
ushort teleportSequence,
|
||||||
uint destinationCell)
|
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"))
|
if (!ValidateActive(generation, destinationCell, "materialized"))
|
||||||
return false;
|
return false;
|
||||||
if (_snapshot.Materialized)
|
if (_snapshot.Materialized)
|
||||||
|
|
@ -247,8 +480,26 @@ public sealed class RuntimeWorldTransitState
|
||||||
public void ResetSession()
|
public void ResetSession()
|
||||||
{
|
{
|
||||||
_snapshot = RuntimePortalSnapshot.Idle;
|
_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(
|
private bool ValidateActive(
|
||||||
long generation,
|
long generation,
|
||||||
uint destinationCell,
|
uint destinationCell,
|
||||||
|
|
@ -360,6 +611,21 @@ public sealed class RuntimeWorldTransitState
|
||||||
+ $"failures={snapshot.InvariantFailureCount}");
|
+ $"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) =>
|
private static bool IsIndoor(uint cellId) =>
|
||||||
(cellId & 0xFFFFu) >= 0x0100u;
|
(cellId & 0xFFFFu) >= 0x0100u;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,9 @@ public sealed class WorldSceneRendererTests
|
||||||
{
|
{
|
||||||
var transit = new RuntimeWorldTransitState();
|
var transit = new RuntimeWorldTransitState();
|
||||||
var availability = new WorldGenerationAvailabilityState(transit);
|
var availability = new WorldGenerationAvailabilityState(transit);
|
||||||
transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u);
|
RuntimeWorldTransitTestDriver.BeginPortal(
|
||||||
|
transit,
|
||||||
|
0x11340021u);
|
||||||
var rig = new Rig(
|
var rig = new Rig(
|
||||||
portalVisible: false,
|
portalVisible: false,
|
||||||
waitingForLogin: false,
|
waitingForLogin: false,
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,13 @@ public sealed class CurrentGameRuntimeAdapterTests
|
||||||
};
|
};
|
||||||
harness.Objects.AddOrUpdate(item);
|
harness.Objects.AddOrUpdate(item);
|
||||||
harness.Chat.OnSystemMessage("runtime parity", 0x1Au);
|
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 =
|
WorldRevealReadinessSnapshot readiness =
|
||||||
harness.WorldReveal.PrepareAndEvaluate(0x12340001u);
|
harness.WorldReveal.PrepareAndEvaluate(0x12340001u);
|
||||||
Assert.True(readiness.IsReady);
|
Assert.True(readiness.IsReady);
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,18 @@ public sealed class RuntimeWorldTransitOwnershipTests
|
||||||
appRoot,
|
appRoot,
|
||||||
"Streaming",
|
"Streaming",
|
||||||
"WorldRevealLifecycleTelemetry.cs")));
|
"WorldRevealLifecycleTelemetry.cs")));
|
||||||
|
Assert.False(File.Exists(Path.Combine(
|
||||||
|
appRoot,
|
||||||
|
"Streaming",
|
||||||
|
"TeleportTransitCoordinator.cs")));
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
"class WorldRevealLifecycleTelemetry",
|
"class WorldRevealLifecycleTelemetry",
|
||||||
app,
|
app,
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
"class TeleportTransitCoordinator",
|
||||||
|
app,
|
||||||
|
StringComparison.Ordinal);
|
||||||
Assert.DoesNotContain(
|
Assert.DoesNotContain(
|
||||||
"record struct WorldRevealLifecycleSnapshot",
|
"record struct WorldRevealLifecycleSnapshot",
|
||||||
app,
|
app,
|
||||||
|
|
@ -74,6 +82,19 @@ public sealed class RuntimeWorldTransitOwnershipTests
|
||||||
field => field.Name is "_activeGeneration"
|
field => field.Name is "_activeGeneration"
|
||||||
or "_worldSimulationReleased"
|
or "_worldSimulationReleased"
|
||||||
or "_lifecycle");
|
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()
|
private static string FindRepositoryRoot()
|
||||||
|
|
|
||||||
53
tests/AcDream.App.Tests/RuntimeWorldTransitTestDriver.cs
Normal file
53
tests/AcDream.App.Tests/RuntimeWorldTransitTestDriver.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -622,7 +622,7 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
var transit = new RuntimeWorldTransitState();
|
var transit = new RuntimeWorldTransitState();
|
||||||
var availability = new WorldGenerationAvailabilityState(transit);
|
var availability = new WorldGenerationAvailabilityState(transit);
|
||||||
long generation =
|
long generation =
|
||||||
transit.BeginReveal(RuntimePortalKind.Portal, cell);
|
RuntimeWorldTransitTestDriver.BeginPortal(transit, cell);
|
||||||
var state = new GpuWorldState(availability: availability);
|
var state = new GpuWorldState(availability: availability);
|
||||||
state.AddLandblock(new LoadedLandblock(
|
state.AddLandblock(new LoadedLandblock(
|
||||||
landblock,
|
landblock,
|
||||||
|
|
@ -658,7 +658,10 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
IsRenderNeighborhoodReady: true,
|
IsRenderNeighborhoodReady: true,
|
||||||
AreCompositeTexturesReady: true,
|
AreCompositeTexturesReady: true,
|
||||||
IsCollisionReady: true)));
|
IsCollisionReady: true)));
|
||||||
Assert.True(transit.AcknowledgeMaterialized(generation, cell));
|
Assert.True(RuntimeWorldTransitTestDriver.MaterializePortal(
|
||||||
|
transit,
|
||||||
|
generation,
|
||||||
|
cell));
|
||||||
Assert.True(transit.Complete(generation));
|
Assert.True(transit.Complete(generation));
|
||||||
Assert.True(state.IsLiveEntityVisible(projectionKey));
|
Assert.True(state.IsLiveEntityVisible(projectionKey));
|
||||||
Assert.True(record.IsSpatiallyVisible);
|
Assert.True(record.IsSpatiallyVisible);
|
||||||
|
|
@ -705,7 +708,9 @@ public sealed class GpuWorldStateVisibilityTests
|
||||||
{
|
{
|
||||||
var transit = new RuntimeWorldTransitState();
|
var transit = new RuntimeWorldTransitState();
|
||||||
var availability = new WorldGenerationAvailabilityState(transit);
|
var availability = new WorldGenerationAvailabilityState(transit);
|
||||||
transit.BeginReveal(RuntimePortalKind.Portal, 0x10100001u);
|
RuntimeWorldTransitTestDriver.BeginPortal(
|
||||||
|
transit,
|
||||||
|
0x10100001u);
|
||||||
var centerStatic = Entity(1, 0u);
|
var centerStatic = Entity(1, 0u);
|
||||||
var centerLive = Entity(2, 0x70000001u);
|
var centerLive = Entity(2, 0x70000001u);
|
||||||
var neighborStatic = Entity(3, 0u);
|
var neighborStatic = Entity(3, 0u);
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
public void DestinationBeforeStart_IsReplayedOnceAfterPresentationActivates()
|
public void DestinationBeforeStart_IsReplayedOnceAfterPresentationActivates()
|
||||||
{
|
{
|
||||||
var harness = new Harness();
|
var harness = new Harness();
|
||||||
WorldSession.EntityPositionUpdate destination = Position(
|
RuntimeTeleportDestination destination = Position(
|
||||||
cellId: 0x20210001u,
|
cellId: 0x20210001u,
|
||||||
teleportSequence: 7,
|
teleportSequence: 7,
|
||||||
x: 12f,
|
x: 12f,
|
||||||
|
|
@ -231,6 +231,24 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
Assert.Equal(1, harness.Reveal.PortalMaterializationCount);
|
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]
|
[Fact]
|
||||||
public void LoginComplete_EntersWorldThenSendsThenCompletesAndResets()
|
public void LoginComplete_EntersWorldThenSendsThenCompletesAndResets()
|
||||||
{
|
{
|
||||||
|
|
@ -565,29 +583,21 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
return int.MaxValue;
|
return int.MaxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static WorldSession.EntityPositionUpdate Position(
|
private static RuntimeTeleportDestination Position(
|
||||||
uint cellId,
|
uint cellId,
|
||||||
ushort teleportSequence,
|
ushort teleportSequence,
|
||||||
float x,
|
float x,
|
||||||
float y,
|
float y,
|
||||||
float z) => new(
|
float z) => new(
|
||||||
0x50000001u,
|
EntityGuid: 0x50000001u,
|
||||||
new CreateObject.ServerPosition(
|
InstanceSequence: 1,
|
||||||
|
PositionSequence: 1,
|
||||||
|
TeleportSequence: teleportSequence,
|
||||||
|
ForcePositionSequence: 1,
|
||||||
|
Position: new Position(
|
||||||
cellId,
|
cellId,
|
||||||
x,
|
new Vector3(x, y, z),
|
||||||
y,
|
Quaternion.Identity));
|
||||||
z,
|
|
||||||
1f,
|
|
||||||
0f,
|
|
||||||
0f,
|
|
||||||
0f),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
teleportSequence,
|
|
||||||
1);
|
|
||||||
|
|
||||||
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell) => new(
|
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell) => new(
|
||||||
Guid: guid,
|
Guid: guid,
|
||||||
|
|
@ -651,6 +661,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
public readonly FakePlacement Placement;
|
public readonly FakePlacement Placement;
|
||||||
public readonly FakeSession Session;
|
public readonly FakeSession Session;
|
||||||
public readonly FakePresentation Presentation;
|
public readonly FakePresentation Presentation;
|
||||||
|
public readonly RuntimeWorldTransitState Transit;
|
||||||
public readonly WorldRevealCoordinator Reveal;
|
public readonly WorldRevealCoordinator Reveal;
|
||||||
public readonly LocalPlayerTeleportController Controller;
|
public readonly LocalPlayerTeleportController Controller;
|
||||||
|
|
||||||
|
|
@ -666,8 +677,9 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
Placement = new FakePlacement(order);
|
Placement = new FakePlacement(order);
|
||||||
Session = new FakeSession(order);
|
Session = new FakeSession(order);
|
||||||
Presentation = new FakePresentation(order);
|
Presentation = new FakePresentation(order);
|
||||||
|
Transit = new RuntimeWorldTransitState(order.Add);
|
||||||
Reveal = new WorldRevealCoordinator(
|
Reveal = new WorldRevealCoordinator(
|
||||||
new RuntimeWorldTransitState(order.Add),
|
Transit,
|
||||||
isRenderNeighborhoodReady: (_, _) => worldReady,
|
isRenderNeighborhoodReady: (_, _) => worldReady,
|
||||||
isSpawnCellReady: _ => worldReady,
|
isSpawnCellReady: _ => worldReady,
|
||||||
isTerrainNeighborhoodReady: (_, _) => worldReady,
|
isTerrainNeighborhoodReady: (_, _) => worldReady,
|
||||||
|
|
@ -681,6 +693,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
Input,
|
Input,
|
||||||
Mode,
|
Mode,
|
||||||
Streaming,
|
Streaming,
|
||||||
|
Transit,
|
||||||
Reveal,
|
Reveal,
|
||||||
Placement,
|
Placement,
|
||||||
Session,
|
Session,
|
||||||
|
|
@ -830,7 +843,7 @@ public sealed class LocalPlayerTeleportControllerTests
|
||||||
public List<uint> Starts { get; } = [];
|
public List<uint> Starts { get; } = [];
|
||||||
public void OnTeleportStarted(uint sequence) => Starts.Add(sequence);
|
public void OnTeleportStarted(uint sequence) => Starts.Add(sequence);
|
||||||
public void OfferDestination(
|
public void OfferDestination(
|
||||||
WorldSession.EntityPositionUpdate update,
|
RuntimeTeleportDestination destination,
|
||||||
bool teleportTimestampAdvanced)
|
bool teleportTimestampAdvanced)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 _));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -37,7 +37,9 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
Assert.Single(world.LandblockBounds);
|
Assert.Single(world.LandblockBounds);
|
||||||
|
|
||||||
long generation =
|
long generation =
|
||||||
transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell);
|
RuntimeWorldTransitTestDriver.BeginPortal(
|
||||||
|
transit,
|
||||||
|
DestinationCell);
|
||||||
world.CopyLiveEntitiesNearLandblock(LandblockId, 0, nearby);
|
world.CopyLiveEntitiesNearLandblock(LandblockId, 0, nearby);
|
||||||
|
|
||||||
Assert.False(availability.IsWorldAvailable);
|
Assert.False(availability.IsWorldAvailable);
|
||||||
|
|
@ -53,7 +55,8 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
Assert.False(availability.IsWorldAvailable);
|
Assert.False(availability.IsWorldAvailable);
|
||||||
Assert.True(transit.AcknowledgeDestinationReadiness(
|
Assert.True(transit.AcknowledgeDestinationReadiness(
|
||||||
Ready(generation, DestinationCell)));
|
Ready(generation, DestinationCell)));
|
||||||
Assert.True(transit.AcknowledgeMaterialized(
|
Assert.True(RuntimeWorldTransitTestDriver.MaterializePortal(
|
||||||
|
transit,
|
||||||
generation,
|
generation,
|
||||||
DestinationCell));
|
DestinationCell));
|
||||||
Assert.True(transit.Complete(generation));
|
Assert.True(transit.Complete(generation));
|
||||||
|
|
@ -87,12 +90,14 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
WorldGenerationQuiescenceEdge firstEdge =
|
WorldGenerationQuiescenceEdge firstEdge =
|
||||||
quiescence.CaptureBegin();
|
quiescence.CaptureBegin();
|
||||||
long first =
|
long first =
|
||||||
transit.BeginReveal(RuntimePortalKind.Login, DestinationCell);
|
transit.BeginLoginReveal(DestinationCell);
|
||||||
quiescence.CommitBegin(firstEdge);
|
quiescence.CommitBegin(firstEdge);
|
||||||
WorldGenerationQuiescenceEdge secondEdge =
|
WorldGenerationQuiescenceEdge secondEdge =
|
||||||
quiescence.CaptureBegin();
|
quiescence.CaptureBegin();
|
||||||
long second =
|
long second =
|
||||||
transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell);
|
RuntimeWorldTransitTestDriver.BeginPortal(
|
||||||
|
transit,
|
||||||
|
DestinationCell);
|
||||||
quiescence.CommitBegin(secondEdge);
|
quiescence.CommitBegin(secondEdge);
|
||||||
|
|
||||||
Assert.False(transit.Complete(first));
|
Assert.False(transit.Complete(first));
|
||||||
|
|
@ -104,7 +109,8 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
|
|
||||||
Assert.True(transit.AcknowledgeDestinationReadiness(
|
Assert.True(transit.AcknowledgeDestinationReadiness(
|
||||||
Ready(second, DestinationCell)));
|
Ready(second, DestinationCell)));
|
||||||
Assert.True(transit.AcknowledgeMaterialized(
|
Assert.True(RuntimeWorldTransitTestDriver.MaterializePortal(
|
||||||
|
transit,
|
||||||
second,
|
second,
|
||||||
DestinationCell));
|
DestinationCell));
|
||||||
Assert.True(transit.Complete(second));
|
Assert.True(transit.Complete(second));
|
||||||
|
|
@ -130,7 +136,9 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
audio: null);
|
audio: null);
|
||||||
|
|
||||||
WorldGenerationQuiescenceEdge edge = quiescence.CaptureBegin();
|
WorldGenerationQuiescenceEdge edge = quiescence.CaptureBegin();
|
||||||
transit.BeginReveal(RuntimePortalKind.Portal, DestinationCell);
|
RuntimeWorldTransitTestDriver.BeginPortal(
|
||||||
|
transit,
|
||||||
|
DestinationCell);
|
||||||
quiescence.CommitBegin(edge);
|
quiescence.CommitBegin(edge);
|
||||||
|
|
||||||
Assert.Equal(inventoryGuid, selection.SelectedObjectId);
|
Assert.Equal(inventoryGuid, selection.SelectedObjectId);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
using System.Numerics;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
using AcDream.Runtime.World;
|
using AcDream.Runtime.World;
|
||||||
|
|
@ -10,6 +12,7 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
{
|
{
|
||||||
private sealed class State
|
private sealed class State
|
||||||
{
|
{
|
||||||
|
public RuntimeWorldTransitState Transit { get; private set; } = null!;
|
||||||
public bool RenderReady { get; set; }
|
public bool RenderReady { get; set; }
|
||||||
public bool CollisionReady { get; set; }
|
public bool CollisionReady { get; set; }
|
||||||
public bool CompositesReady { get; set; }
|
public bool CompositesReady { get; set; }
|
||||||
|
|
@ -24,6 +27,7 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
{
|
{
|
||||||
transit ??= new RuntimeWorldTransitState(
|
transit ??= new RuntimeWorldTransitState(
|
||||||
logs is null ? null : new Action<string>(logs.Add));
|
logs is null ? null : new Action<string>(logs.Add));
|
||||||
|
Transit = transit;
|
||||||
return new WorldRevealCoordinator(
|
return new WorldRevealCoordinator(
|
||||||
transit,
|
transit,
|
||||||
isRenderNeighborhoodReady: (_, _) => RenderReady,
|
isRenderNeighborhoodReady: (_, _) => RenderReady,
|
||||||
|
|
@ -48,7 +52,7 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
var state = new State { CompositesReady = true };
|
var state = new State { CompositesReady = true };
|
||||||
WorldRevealCoordinator coordinator = state.Build();
|
WorldRevealCoordinator coordinator = state.Build();
|
||||||
|
|
||||||
long generation = coordinator.Begin(RuntimePortalKind.Login, 0x11340021u);
|
long generation = coordinator.BeginLogin(0x11340021u);
|
||||||
|
|
||||||
Assert.Equal(1, generation);
|
Assert.Equal(1, generation);
|
||||||
Assert.Equal(1, state.InvalidateCount);
|
Assert.Equal(1, state.InvalidateCount);
|
||||||
|
|
@ -66,7 +70,7 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
CollisionReady = true,
|
CollisionReady = true,
|
||||||
};
|
};
|
||||||
WorldRevealCoordinator coordinator = state.Build();
|
WorldRevealCoordinator coordinator = state.Build();
|
||||||
coordinator.Begin(RuntimePortalKind.Login, outdoorCell);
|
coordinator.BeginLogin(outdoorCell);
|
||||||
|
|
||||||
WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell);
|
WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell);
|
||||||
state.CompositesReady = true;
|
state.CompositesReady = true;
|
||||||
|
|
@ -91,12 +95,16 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
CompositesReady = true,
|
CompositesReady = true,
|
||||||
};
|
};
|
||||||
WorldRevealCoordinator coordinator = state.Build(logs);
|
WorldRevealCoordinator coordinator = state.Build(logs);
|
||||||
coordinator.Begin(RuntimePortalKind.Portal, outdoorCell);
|
long generation = BeginPortal(
|
||||||
|
coordinator,
|
||||||
|
state.Transit,
|
||||||
|
outdoorCell,
|
||||||
|
sequence: 1);
|
||||||
state.CompositesReady = true;
|
state.CompositesReady = true;
|
||||||
|
|
||||||
Assert.True(coordinator.Evaluate(outdoorCell).IsReady);
|
Assert.True(coordinator.Evaluate(outdoorCell).IsReady);
|
||||||
coordinator.ObserveMaterialized();
|
coordinator.ObserveMaterialized(generation, 1, outdoorCell);
|
||||||
coordinator.ObserveMaterialized();
|
coordinator.ObserveMaterialized(generation, 1, outdoorCell);
|
||||||
coordinator.ObserveWorldViewportVisible();
|
coordinator.ObserveWorldViewportVisible();
|
||||||
coordinator.Complete();
|
coordinator.Complete();
|
||||||
|
|
||||||
|
|
@ -108,6 +116,40 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
Assert.Single(logs, line => line.Contains("event=materialized", StringComparison.Ordinal));
|
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]
|
[Fact]
|
||||||
public void Cancel_PreventsLateDestinationWorkFromMutatingLifecycle()
|
public void Cancel_PreventsLateDestinationWorkFromMutatingLifecycle()
|
||||||
{
|
{
|
||||||
|
|
@ -118,11 +160,18 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
CompositesReady = true,
|
CompositesReady = true,
|
||||||
};
|
};
|
||||||
WorldRevealCoordinator coordinator = state.Build();
|
WorldRevealCoordinator coordinator = state.Build();
|
||||||
coordinator.Begin(RuntimePortalKind.Portal, 0x11340021u);
|
long generation = BeginPortal(
|
||||||
|
coordinator,
|
||||||
|
state.Transit,
|
||||||
|
0x11340021u,
|
||||||
|
sequence: 1);
|
||||||
coordinator.Cancel();
|
coordinator.Cancel();
|
||||||
|
|
||||||
coordinator.PrepareAndEvaluate(0x11340021u);
|
coordinator.PrepareAndEvaluate(0x11340021u);
|
||||||
coordinator.ObserveMaterialized();
|
coordinator.ObserveMaterialized(
|
||||||
|
generation,
|
||||||
|
1,
|
||||||
|
0x11340021u);
|
||||||
coordinator.ObserveWorldViewportVisible();
|
coordinator.ObserveWorldViewportVisible();
|
||||||
coordinator.Complete();
|
coordinator.Complete();
|
||||||
|
|
||||||
|
|
@ -155,14 +204,22 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
isSpawnClaimUnhydratable: _ => false,
|
isSpawnClaimUnhydratable: _ => false,
|
||||||
quiescence: quiescence);
|
quiescence: quiescence);
|
||||||
|
|
||||||
Assert.Equal(1, coordinator.Begin(RuntimePortalKind.Login, 0x11340021u));
|
Assert.Equal(1, coordinator.BeginLogin(0x11340021u));
|
||||||
Assert.Equal(2, coordinator.Begin(RuntimePortalKind.Portal, 0x11340021u));
|
long generation = BeginPortal(
|
||||||
|
coordinator,
|
||||||
|
transit,
|
||||||
|
0x11340021u,
|
||||||
|
sequence: 1);
|
||||||
|
Assert.Equal(2, generation);
|
||||||
Assert.False(availability.IsWorldAvailable);
|
Assert.False(availability.IsWorldAvailable);
|
||||||
Assert.Equal(2, availability.QuiescedGeneration);
|
Assert.Equal(2, availability.QuiescedGeneration);
|
||||||
Assert.Equal(1, audio.SuspendCalls);
|
Assert.Equal(1, audio.SuspendCalls);
|
||||||
|
|
||||||
Assert.True(coordinator.Evaluate(0x11340021u).IsReady);
|
Assert.True(coordinator.Evaluate(0x11340021u).IsReady);
|
||||||
coordinator.ObserveMaterialized();
|
coordinator.ObserveMaterialized(
|
||||||
|
generation,
|
||||||
|
1,
|
||||||
|
0x11340021u);
|
||||||
coordinator.Complete();
|
coordinator.Complete();
|
||||||
|
|
||||||
Assert.True(availability.IsWorldAvailable);
|
Assert.True(availability.IsWorldAvailable);
|
||||||
|
|
@ -193,14 +250,21 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
isSpawnClaimUnhydratable: _ => false,
|
isSpawnClaimUnhydratable: _ => false,
|
||||||
quiescence: quiescence,
|
quiescence: quiescence,
|
||||||
streaming: scheduler);
|
streaming: scheduler);
|
||||||
long generation = coordinator.Begin(
|
const uint destinationCell = 0x3032001Cu;
|
||||||
RuntimePortalKind.Portal,
|
const ushort teleportSequence = 1;
|
||||||
0x3032001Cu);
|
long generation = BeginPortal(
|
||||||
|
coordinator,
|
||||||
|
transit,
|
||||||
|
destinationCell,
|
||||||
|
teleportSequence);
|
||||||
|
|
||||||
Assert.False(availability.IsWorldAvailable);
|
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.True(availability.IsWorldAvailable);
|
||||||
Assert.False(coordinator.Snapshot.WorldViewportObserved);
|
Assert.False(coordinator.Snapshot.WorldViewportObserved);
|
||||||
|
|
@ -259,13 +323,21 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
() => calls.Add("spatial")),
|
() => calls.Add("spatial")),
|
||||||
availability);
|
availability);
|
||||||
|
|
||||||
coordinator.Begin(RuntimePortalKind.Portal, 0x3032001Cu);
|
const uint destinationCell = 0x3032001Cu;
|
||||||
|
long generation = BeginPortal(
|
||||||
|
coordinator,
|
||||||
|
transit,
|
||||||
|
destinationCell,
|
||||||
|
sequence: 1);
|
||||||
frame.Tick(1f / 60f);
|
frame.Tick(1f / 60f);
|
||||||
Assert.Equal(["network", "commands"], calls);
|
Assert.Equal(["network", "commands"], calls);
|
||||||
|
|
||||||
calls.Clear();
|
calls.Clear();
|
||||||
Assert.True(coordinator.Evaluate(0x3032001Cu).IsReady);
|
Assert.True(coordinator.Evaluate(destinationCell).IsReady);
|
||||||
coordinator.ObserveMaterialized();
|
coordinator.ObserveMaterialized(
|
||||||
|
generation,
|
||||||
|
1,
|
||||||
|
destinationCell);
|
||||||
frame.Tick(1f / 60f);
|
frame.Tick(1f / 60f);
|
||||||
|
|
||||||
Assert.Equal(["objects", "network", "commands", "spatial"], calls);
|
Assert.Equal(["objects", "network", "commands", "spatial"], calls);
|
||||||
|
|
@ -297,9 +369,11 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
isSpawnClaimUnhydratable: _ => false,
|
isSpawnClaimUnhydratable: _ => false,
|
||||||
quiescence: quiescence,
|
quiescence: quiescence,
|
||||||
streaming: scheduler);
|
streaming: scheduler);
|
||||||
long generation = coordinator.Begin(
|
long generation = BeginPortal(
|
||||||
RuntimePortalKind.Portal,
|
coordinator,
|
||||||
0x3032001Cu);
|
transit,
|
||||||
|
0x3032001Cu,
|
||||||
|
sequence: 1);
|
||||||
|
|
||||||
coordinator.ResetSession();
|
coordinator.ResetSession();
|
||||||
|
|
||||||
|
|
@ -317,12 +391,17 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
var scheduler = new RecordingDestinationScheduler();
|
var scheduler = new RecordingDestinationScheduler();
|
||||||
WorldRevealCoordinator coordinator = state.Build(streaming: scheduler);
|
WorldRevealCoordinator coordinator = state.Build(streaming: scheduler);
|
||||||
|
|
||||||
long first = coordinator.Begin(
|
long first = BeginPortal(
|
||||||
RuntimePortalKind.Portal,
|
coordinator,
|
||||||
0x11340021u);
|
state.Transit,
|
||||||
long second = coordinator.Begin(
|
0x11340021u,
|
||||||
RuntimePortalKind.Portal,
|
sequence: 1);
|
||||||
0x20210123u);
|
state.Transit.EndTeleport();
|
||||||
|
long second = BeginPortal(
|
||||||
|
coordinator,
|
||||||
|
state.Transit,
|
||||||
|
0x20210123u,
|
||||||
|
sequence: 2);
|
||||||
|
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
[
|
[
|
||||||
|
|
@ -342,12 +421,17 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
WorldRevealCoordinator coordinator =
|
WorldRevealCoordinator coordinator =
|
||||||
state.Build(renderResources: resources);
|
state.Build(renderResources: resources);
|
||||||
|
|
||||||
long first = coordinator.Begin(
|
long first = BeginPortal(
|
||||||
RuntimePortalKind.Portal,
|
coordinator,
|
||||||
0x11340021u);
|
state.Transit,
|
||||||
long second = coordinator.Begin(
|
0x11340021u,
|
||||||
RuntimePortalKind.Portal,
|
sequence: 1);
|
||||||
0x20210123u);
|
state.Transit.EndTeleport();
|
||||||
|
long second = BeginPortal(
|
||||||
|
coordinator,
|
||||||
|
state.Transit,
|
||||||
|
0x20210123u,
|
||||||
|
sequence: 2);
|
||||||
|
|
||||||
Assert.Equal([first, second], resources.Begins);
|
Assert.Equal([first, second], resources.Begins);
|
||||||
coordinator.RevealWorldViewport();
|
coordinator.RevealWorldViewport();
|
||||||
|
|
@ -356,6 +440,33 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
Assert.Equal([second], resources.Ends);
|
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
|
private sealed class RecordingAudioQuiescence : AcDream.App.Audio.IWorldAudioQuiescence
|
||||||
{
|
{
|
||||||
public int SuspendCalls { get; private set; }
|
public int SuspendCalls { get; private set; }
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,9 @@ public sealed class RecallTeleportAnimationTests
|
||||||
var calls = new List<string>();
|
var calls = new List<string>();
|
||||||
var transit = new RuntimeWorldTransitState();
|
var transit = new RuntimeWorldTransitState();
|
||||||
var availability = new WorldGenerationAvailabilityState(transit);
|
var availability = new WorldGenerationAvailabilityState(transit);
|
||||||
transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u);
|
RuntimeWorldTransitTestDriver.BeginPortal(
|
||||||
|
transit,
|
||||||
|
0x11340021u);
|
||||||
var frame = new RetailLiveFrameCoordinator(
|
var frame = new RetailLiveFrameCoordinator(
|
||||||
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
|
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
|
||||||
new GpuWorldState(availability: availability),
|
new GpuWorldState(availability: availability),
|
||||||
|
|
@ -66,7 +68,9 @@ public sealed class RecallTeleportAnimationTests
|
||||||
var calls = new List<string>();
|
var calls = new List<string>();
|
||||||
var transit = new RuntimeWorldTransitState();
|
var transit = new RuntimeWorldTransitState();
|
||||||
var availability = new WorldGenerationAvailabilityState(transit);
|
var availability = new WorldGenerationAvailabilityState(transit);
|
||||||
transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u);
|
RuntimeWorldTransitTestDriver.BeginPortal(
|
||||||
|
transit,
|
||||||
|
0x11340021u);
|
||||||
var frame = new RetailLiveFrameCoordinator(
|
var frame = new RetailLiveFrameCoordinator(
|
||||||
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
|
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
|
||||||
new GpuWorldState(availability: availability),
|
new GpuWorldState(availability: availability),
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,9 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
var transit = new RuntimeWorldTransitState();
|
var transit = new RuntimeWorldTransitState();
|
||||||
var availability = new WorldGenerationAvailabilityState(transit);
|
var availability = new WorldGenerationAvailabilityState(transit);
|
||||||
long generation =
|
long generation =
|
||||||
transit.BeginReveal(RuntimePortalKind.Portal, 0x11340021u);
|
RuntimeWorldTransitTestDriver.BeginPortal(
|
||||||
|
transit,
|
||||||
|
0x11340021u);
|
||||||
UpdateFrameOrchestrator frame = Create(
|
UpdateFrameOrchestrator frame = Create(
|
||||||
calls,
|
calls,
|
||||||
observed: observed,
|
observed: observed,
|
||||||
|
|
@ -160,7 +162,8 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
IsRenderNeighborhoodReady: true,
|
IsRenderNeighborhoodReady: true,
|
||||||
AreCompositeTexturesReady: true,
|
AreCompositeTexturesReady: true,
|
||||||
IsCollisionReady: true)));
|
IsCollisionReady: true)));
|
||||||
Assert.True(transit.AcknowledgeMaterialized(
|
Assert.True(RuntimeWorldTransitTestDriver.MaterializePortal(
|
||||||
|
transit,
|
||||||
generation,
|
generation,
|
||||||
0x11340021u));
|
0x11340021u));
|
||||||
transit.Complete(generation);
|
transit.Complete(generation);
|
||||||
|
|
@ -514,9 +517,9 @@ public sealed class UpdateFrameOrchestratorTests
|
||||||
"LocalPlayerTeleportController.cs"));
|
"LocalPlayerTeleportController.cs"));
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
teleportSource,
|
teleportSource,
|
||||||
"_transit.CanBegin(teleportSequence)",
|
"_transit.CanQueueTeleportStart(teleportSequence)",
|
||||||
"_input.EndMouseLook();",
|
"_input.EndMouseLook();",
|
||||||
"_transit.QueueStart(teleportSequence)");
|
"_transit.TryQueueTeleportStart(teleportSequence)");
|
||||||
string playerModeSource = File.ReadAllText(Path.Combine(
|
string playerModeSource = File.ReadAllText(Path.Combine(
|
||||||
FindRepoRoot(),
|
FindRepoRoot(),
|
||||||
"src",
|
"src",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Runtime.World;
|
using AcDream.Runtime.World;
|
||||||
|
|
||||||
namespace AcDream.Runtime.Tests.World;
|
namespace AcDream.Runtime.Tests.World;
|
||||||
|
|
@ -13,7 +15,7 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
|
|
||||||
long generation =
|
long generation =
|
||||||
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
|
state.BeginLoginReveal(OutdoorCell);
|
||||||
|
|
||||||
Assert.Equal(1, generation);
|
Assert.Equal(1, generation);
|
||||||
Assert.Equal(generation, state.Snapshot.Generation);
|
Assert.Equal(generation, state.Snapshot.Generation);
|
||||||
|
|
@ -29,7 +31,7 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
List<string> log = [];
|
List<string> log = [];
|
||||||
var state = new RuntimeWorldTransitState(log.Add);
|
var state = new RuntimeWorldTransitState(log.Add);
|
||||||
long generation =
|
long generation =
|
||||||
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
|
BeginPortal(state, OutdoorCell);
|
||||||
|
|
||||||
Assert.False(state.AcknowledgeDestinationReadiness(
|
Assert.False(state.AcknowledgeDestinationReadiness(
|
||||||
Ready(generation + 1, OutdoorCell)));
|
Ready(generation + 1, OutdoorCell)));
|
||||||
|
|
@ -56,12 +58,13 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
{
|
{
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
long generation =
|
long generation =
|
||||||
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
|
BeginPortal(state, OutdoorCell);
|
||||||
Assert.True(state.AcknowledgeDestinationReadiness(
|
Assert.True(state.AcknowledgeDestinationReadiness(
|
||||||
Ready(generation, OutdoorCell)));
|
Ready(generation, OutdoorCell)));
|
||||||
|
|
||||||
Assert.True(state.AcknowledgeMaterialized(
|
Assert.True(state.AcknowledgePortalMaterialized(
|
||||||
generation,
|
generation,
|
||||||
|
1,
|
||||||
OutdoorCell));
|
OutdoorCell));
|
||||||
|
|
||||||
Assert.True(state.IsWorldSimulationAvailable);
|
Assert.True(state.IsWorldSimulationAvailable);
|
||||||
|
|
@ -69,8 +72,9 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
Assert.False(state.Snapshot.WorldViewportObserved);
|
Assert.False(state.Snapshot.WorldViewportObserved);
|
||||||
Assert.False(state.Snapshot.Completed);
|
Assert.False(state.Snapshot.Completed);
|
||||||
Assert.Equal(1, state.Snapshot.PortalMaterializationCount);
|
Assert.Equal(1, state.Snapshot.PortalMaterializationCount);
|
||||||
Assert.False(state.AcknowledgeMaterialized(
|
Assert.False(state.AcknowledgePortalMaterialized(
|
||||||
generation,
|
generation,
|
||||||
|
1,
|
||||||
OutdoorCell));
|
OutdoorCell));
|
||||||
|
|
||||||
Assert.True(state.AcknowledgeWorldViewportVisible(generation));
|
Assert.True(state.AcknowledgeWorldViewportVisible(generation));
|
||||||
|
|
@ -85,10 +89,11 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
{
|
{
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
long generation =
|
long generation =
|
||||||
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
|
BeginPortal(state, OutdoorCell);
|
||||||
|
|
||||||
Assert.False(state.AcknowledgeMaterialized(
|
Assert.False(state.AcknowledgePortalMaterialized(
|
||||||
generation,
|
generation,
|
||||||
|
1,
|
||||||
OutdoorCell));
|
OutdoorCell));
|
||||||
|
|
||||||
Assert.False(state.IsWorldSimulationAvailable);
|
Assert.False(state.IsWorldSimulationAvailable);
|
||||||
|
|
@ -102,7 +107,7 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
{
|
{
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
long generation =
|
long generation =
|
||||||
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
|
state.BeginLoginReveal(OutdoorCell);
|
||||||
state.AcknowledgeDestinationReadiness(
|
state.AcknowledgeDestinationReadiness(
|
||||||
Ready(generation, OutdoorCell));
|
Ready(generation, OutdoorCell));
|
||||||
|
|
||||||
|
|
@ -120,7 +125,7 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
{
|
{
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
long generation =
|
long generation =
|
||||||
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
|
state.BeginLoginReveal(OutdoorCell);
|
||||||
|
|
||||||
Assert.False(state.AcknowledgeWorldViewportVisible(generation));
|
Assert.False(state.AcknowledgeWorldViewportVisible(generation));
|
||||||
|
|
||||||
|
|
@ -134,7 +139,7 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
{
|
{
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
long generation =
|
long generation =
|
||||||
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
|
state.BeginLoginReveal(OutdoorCell);
|
||||||
RuntimeDestinationReadiness invalid =
|
RuntimeDestinationReadiness invalid =
|
||||||
Ready(generation, OutdoorCell) with
|
Ready(generation, OutdoorCell) with
|
||||||
{
|
{
|
||||||
|
|
@ -153,7 +158,7 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
{
|
{
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
long generation =
|
long generation =
|
||||||
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
|
BeginPortal(state, OutdoorCell);
|
||||||
RuntimeDestinationReadiness accepted =
|
RuntimeDestinationReadiness accepted =
|
||||||
Ready(generation, OutdoorCell);
|
Ready(generation, OutdoorCell);
|
||||||
Assert.True(state.AcknowledgeDestinationReadiness(accepted));
|
Assert.True(state.AcknowledgeDestinationReadiness(accepted));
|
||||||
|
|
@ -174,13 +179,13 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
{
|
{
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
long login =
|
long login =
|
||||||
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
|
state.BeginLoginReveal(OutdoorCell);
|
||||||
|
|
||||||
Assert.False(state.Complete(login));
|
Assert.False(state.Complete(login));
|
||||||
Assert.False(state.Snapshot.Completed);
|
Assert.False(state.Snapshot.Completed);
|
||||||
|
|
||||||
long portal =
|
long portal =
|
||||||
state.BeginReveal(RuntimePortalKind.Portal, OtherCell);
|
BeginPortal(state, OtherCell, sequence: 2);
|
||||||
Assert.True(state.AcknowledgeDestinationReadiness(
|
Assert.True(state.AcknowledgeDestinationReadiness(
|
||||||
Ready(portal, OtherCell)));
|
Ready(portal, OtherCell)));
|
||||||
Assert.False(state.Complete(portal));
|
Assert.False(state.Complete(portal));
|
||||||
|
|
@ -193,14 +198,17 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
{
|
{
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
long first =
|
long first =
|
||||||
state.BeginReveal(RuntimePortalKind.Login, OutdoorCell);
|
state.BeginLoginReveal(OutdoorCell);
|
||||||
long second =
|
long second =
|
||||||
state.BeginReveal(RuntimePortalKind.Portal, OtherCell);
|
BeginPortal(state, OtherCell);
|
||||||
|
|
||||||
Assert.True(second > first);
|
Assert.True(second > first);
|
||||||
Assert.False(state.AcknowledgeDestinationReadiness(
|
Assert.False(state.AcknowledgeDestinationReadiness(
|
||||||
Ready(first, OutdoorCell)));
|
Ready(first, OutdoorCell)));
|
||||||
Assert.False(state.AcknowledgeMaterialized(first, OutdoorCell));
|
Assert.False(state.AcknowledgePortalMaterialized(
|
||||||
|
first,
|
||||||
|
1,
|
||||||
|
OutdoorCell));
|
||||||
|
|
||||||
Assert.Equal(second, state.Snapshot.Generation);
|
Assert.Equal(second, state.Snapshot.Generation);
|
||||||
Assert.Equal(OtherCell, state.Snapshot.DestinationCell);
|
Assert.Equal(OtherCell, state.Snapshot.DestinationCell);
|
||||||
|
|
@ -214,7 +222,7 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
{
|
{
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
long generation =
|
long generation =
|
||||||
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
|
BeginPortal(state, OutdoorCell);
|
||||||
|
|
||||||
Assert.True(state.Cancel(generation));
|
Assert.True(state.Cancel(generation));
|
||||||
Assert.True(state.IsWorldSimulationAvailable);
|
Assert.True(state.IsWorldSimulationAvailable);
|
||||||
|
|
@ -222,8 +230,9 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
|
|
||||||
Assert.False(state.AcknowledgeDestinationReadiness(
|
Assert.False(state.AcknowledgeDestinationReadiness(
|
||||||
Ready(generation, OutdoorCell)));
|
Ready(generation, OutdoorCell)));
|
||||||
Assert.False(state.AcknowledgeMaterialized(
|
Assert.False(state.AcknowledgePortalMaterialized(
|
||||||
generation,
|
generation,
|
||||||
|
1,
|
||||||
OutdoorCell));
|
OutdoorCell));
|
||||||
Assert.False(state.Complete(generation));
|
Assert.False(state.Complete(generation));
|
||||||
Assert.False(state.Snapshot.Materialized);
|
Assert.False(state.Snapshot.Materialized);
|
||||||
|
|
@ -235,10 +244,13 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
{
|
{
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
long generation =
|
long generation =
|
||||||
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
|
BeginPortal(state, OutdoorCell);
|
||||||
state.AcknowledgeDestinationReadiness(
|
state.AcknowledgeDestinationReadiness(
|
||||||
Ready(generation, OutdoorCell));
|
Ready(generation, OutdoorCell));
|
||||||
state.AcknowledgeMaterialized(generation, OutdoorCell);
|
state.AcknowledgePortalMaterialized(
|
||||||
|
generation,
|
||||||
|
1,
|
||||||
|
OutdoorCell);
|
||||||
|
|
||||||
Assert.True(state.Cancel(generation));
|
Assert.True(state.Cancel(generation));
|
||||||
Assert.True(state.IsWorldSimulationAvailable);
|
Assert.True(state.IsWorldSimulationAvailable);
|
||||||
|
|
@ -254,7 +266,7 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
{
|
{
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
long generation =
|
long generation =
|
||||||
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
|
BeginPortal(state, OutdoorCell);
|
||||||
|
|
||||||
Assert.False(state.ObserveWait(
|
Assert.False(state.ObserveWait(
|
||||||
generation,
|
generation,
|
||||||
|
|
@ -278,7 +290,7 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
{
|
{
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
long generation =
|
long generation =
|
||||||
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
|
BeginPortal(state, OutdoorCell);
|
||||||
var acknowledgement = new RuntimeDestinationReadiness(
|
var acknowledgement = new RuntimeDestinationReadiness(
|
||||||
generation,
|
generation,
|
||||||
OutdoorCell,
|
OutdoorCell,
|
||||||
|
|
@ -291,8 +303,9 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
|
|
||||||
Assert.True(state.AcknowledgeDestinationReadiness(acknowledgement));
|
Assert.True(state.AcknowledgeDestinationReadiness(acknowledgement));
|
||||||
Assert.True(state.Snapshot.IsReady);
|
Assert.True(state.Snapshot.IsReady);
|
||||||
Assert.True(state.AcknowledgeMaterialized(
|
Assert.True(state.AcknowledgePortalMaterialized(
|
||||||
generation,
|
generation,
|
||||||
|
1,
|
||||||
OutdoorCell));
|
OutdoorCell));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,14 +314,14 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
{
|
{
|
||||||
var state = new RuntimeWorldTransitState();
|
var state = new RuntimeWorldTransitState();
|
||||||
long first =
|
long first =
|
||||||
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
|
BeginPortal(state, OutdoorCell);
|
||||||
state.ResetSession();
|
state.ResetSession();
|
||||||
|
|
||||||
Assert.Equal(RuntimePortalSnapshot.Idle, state.Snapshot);
|
Assert.Equal(RuntimePortalSnapshot.Idle, state.Snapshot);
|
||||||
Assert.True(state.IsWorldSimulationAvailable);
|
Assert.True(state.IsWorldSimulationAvailable);
|
||||||
|
|
||||||
long second =
|
long second =
|
||||||
state.BeginReveal(RuntimePortalKind.Login, OtherCell);
|
state.BeginLoginReveal(OtherCell);
|
||||||
Assert.True(second > first);
|
Assert.True(second > first);
|
||||||
Assert.False(state.AcknowledgeDestinationReadiness(
|
Assert.False(state.AcknowledgeDestinationReadiness(
|
||||||
Ready(first, OutdoorCell)));
|
Ready(first, OutdoorCell)));
|
||||||
|
|
@ -322,12 +335,15 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
var first = new RuntimeWorldTransitState();
|
var first = new RuntimeWorldTransitState();
|
||||||
var second = new RuntimeWorldTransitState();
|
var second = new RuntimeWorldTransitState();
|
||||||
long firstGeneration =
|
long firstGeneration =
|
||||||
first.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
|
BeginPortal(first, OutdoorCell);
|
||||||
long secondGeneration =
|
long secondGeneration =
|
||||||
second.BeginReveal(RuntimePortalKind.Login, OtherCell);
|
second.BeginLoginReveal(OtherCell);
|
||||||
first.AcknowledgeDestinationReadiness(
|
first.AcknowledgeDestinationReadiness(
|
||||||
Ready(firstGeneration, OutdoorCell));
|
Ready(firstGeneration, OutdoorCell));
|
||||||
first.AcknowledgeMaterialized(firstGeneration, OutdoorCell);
|
first.AcknowledgePortalMaterialized(
|
||||||
|
firstGeneration,
|
||||||
|
1,
|
||||||
|
OutdoorCell);
|
||||||
|
|
||||||
Assert.Equal(1, first.Snapshot.PortalMaterializationCount);
|
Assert.Equal(1, first.Snapshot.PortalMaterializationCount);
|
||||||
Assert.Equal(0, second.Snapshot.PortalMaterializationCount);
|
Assert.Equal(0, second.Snapshot.PortalMaterializationCount);
|
||||||
|
|
@ -344,11 +360,12 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
_ => throw new InvalidOperationException("diagnostic failed"));
|
_ => throw new InvalidOperationException("diagnostic failed"));
|
||||||
|
|
||||||
long generation =
|
long generation =
|
||||||
state.BeginReveal(RuntimePortalKind.Portal, OutdoorCell);
|
BeginPortal(state, OutdoorCell);
|
||||||
Assert.True(state.AcknowledgeDestinationReadiness(
|
Assert.True(state.AcknowledgeDestinationReadiness(
|
||||||
Ready(generation, OutdoorCell)));
|
Ready(generation, OutdoorCell)));
|
||||||
Assert.True(state.AcknowledgeMaterialized(
|
Assert.True(state.AcknowledgePortalMaterialized(
|
||||||
generation,
|
generation,
|
||||||
|
1,
|
||||||
OutdoorCell));
|
OutdoorCell));
|
||||||
Assert.True(state.Complete(generation));
|
Assert.True(state.Complete(generation));
|
||||||
|
|
||||||
|
|
@ -359,6 +376,278 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
state.LastDiagnosticFailure);
|
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(
|
private static RuntimeDestinationReadiness Ready(
|
||||||
long generation,
|
long generation,
|
||||||
uint cell) =>
|
uint cell) =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue