feat(app): project canonical runtime placements

This commit is contained in:
Erik 2026-08-01 15:00:49 +02:00
parent ef43667872
commit 74c9b155bd
5 changed files with 1395 additions and 0 deletions

View file

@ -5,6 +5,7 @@ using AcDream.Core.Physics;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
using System.Numerics;
using System.Runtime.ExceptionServices;
@ -386,6 +387,15 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
private readonly Dictionary<RuntimeEntityKey, ILiveEntityAnimationRuntime> _spatialAnimations = new();
private readonly List<RuntimeEntityRecord> _spatialRootCanonicalScratch = new();
private readonly List<RuntimeEntityRecord> _spatialRemoteCanonicalScratch = new();
// Runtime placement receipts are presentation observations of an already
// committed canonical SetPosition transaction. GpuWorldState still emits
// its ordinary visibility callback while that observation rebuckets or
// withdraws a sidecar, so pin the exact incarnation while the callback is
// in flight. A depth (rather than a bool) preserves nested/re-entrant
// projection mutations without ever turning graphical visibility into a
// Runtime physics/workset mutation.
private readonly Dictionary<RuntimeEntityKey, int>
_presentationOnlySpatialMutationDepth = new();
private bool _isClearing;
private bool _sessionClearPendingFinalization;
private bool _isRegisteringResources;
@ -446,6 +456,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
_projections.VisibleRecords;
internal IReadOnlyCollection<RuntimeEntityRecord> CanonicalRecords =>
_directory.ActiveRecords;
internal ulong SessionLifetimeVersion => _directory.SessionLifetimeVersion;
internal RuntimePhysicsState Physics => _physics;
public IReadOnlyDictionary<uint, WorldSession.EntitySpawn> Snapshots => _directory.Snapshots;
internal int AnimationRuntimeCount
@ -863,6 +874,269 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
return true;
}
/// <summary>
/// Applies one canonical Runtime placement receipt to the graphical
/// sidecar only. Runtime has already committed identity, position,
/// collision residence, object-clock state, and simulation worksets.
/// </summary>
internal bool TryApplyRuntimePlacementProjection(
in RuntimePlacementProjectionSnapshot projection)
{
if (projection.Kind is RuntimePlacementProjectionKind.Discard)
{
// Discard cancels only an unacknowledged observation. If its Place
// was already projected, retail keeps that last committed frame
// visible until a later canonical Place or Withdraw supersedes it.
return true;
}
RuntimePlacementProjectionToken token = projection.Token;
if (!TryGetRuntimePlacementProjectionRecord(
token,
requirePlacementVersions:
projection.Kind is RuntimePlacementProjectionKind.Place,
out LiveEntityRecord? record)
|| record.WorldEntity is not { } entity)
{
return false;
}
if (projection.Kind is RuntimePlacementProjectionKind.Place
&& !_spatial.IsLoaded(
(token.ExactCellId & 0xFFFF0000u) | 0xFFFFu))
{
// A canonical SetPosition receipt is not permission to create a
// pending graphical bucket. Keep the FIFO head unacknowledged
// until the destination backend exists, otherwise GpuWorldState's
// later pending-drain edge escapes this exact receipt transaction.
return false;
}
return projection.Kind switch
{
RuntimePlacementProjectionKind.Place =>
TryApplyRuntimePlacementPlace(in projection, record, entity),
RuntimePlacementProjectionKind.Withdraw =>
TryApplyRuntimePlacementWithdrawal(token, record, entity),
_ => false,
};
}
private bool TryApplyRuntimePlacementPlace(
in RuntimePlacementProjectionSnapshot projection,
LiveEntityRecord record,
WorldEntity entity)
{
RuntimePlacementProjectionToken token = projection.Token;
RuntimeEntityKey key = token.Entity;
ulong projectionOperation = ++record.ProjectionMutationVersion;
entity.SetPosition(projection.WorldPosition);
entity.Rotation = projection.Orientation;
entity.ParentCellId = token.ExactCellId;
entity.EffectCellId = token.ExactCellId;
record.IsSpatiallyProjected = true;
Exception? spatialNotificationFailure = null;
uint priorRebucketingGuid = _rebucketingGuid;
_rebucketingGuid = record.ServerGuid;
BeginPresentationOnlySpatialMutation(key);
try
{
try
{
_spatial.RebucketLiveEntity(key, entity, token.ExactCellId);
}
catch (AggregateException error)
{
spatialNotificationFailure = error;
}
}
finally
{
EndPresentationOnlySpatialMutation(key);
_rebucketingGuid = priorRebucketingGuid;
}
if (!IsCurrentProjectionOperation(
record.ServerGuid,
record,
projectionOperation)
|| !TryGetRuntimePlacementProjectionRecord(
token,
requirePlacementVersions: true,
out LiveEntityRecord? current)
|| !ReferenceEquals(current, record)
|| !ReferenceEquals(current.WorldEntity, entity))
{
ThrowAfterCommittedProjectionChange(
record.ServerGuid,
spatialNotificationFailure,
runtimeNotificationFailure: null);
return false;
}
bool visible = _spatial.IsLiveEntityProjectionResident(key);
record.IsSpatiallyVisible = visible;
RefreshSpatialPresentationIndexes(record);
RefreshPresentation(record);
if (!IsCurrentProjectionOperation(
record.ServerGuid,
record,
projectionOperation))
{
ThrowAfterCommittedProjectionChange(
record.ServerGuid,
spatialNotificationFailure,
runtimeNotificationFailure: null);
return false;
}
ThrowAfterCommittedProjectionChange(
record.ServerGuid,
spatialNotificationFailure,
runtimeNotificationFailure: null);
return true;
}
private bool TryApplyRuntimePlacementWithdrawal(
in RuntimePlacementProjectionToken token,
LiveEntityRecord record,
WorldEntity entity)
{
RuntimeEntityKey key = token.Entity;
ulong projectionOperation = ++record.ProjectionMutationVersion;
record.IsSpatiallyProjected = false;
Exception? spatialNotificationFailure = null;
uint priorRebucketingGuid = _rebucketingGuid;
_rebucketingGuid = record.ServerGuid;
BeginPresentationOnlySpatialMutation(key);
try
{
try
{
_spatial.RemoveLiveEntityProjection(entity);
}
catch (AggregateException error)
{
spatialNotificationFailure = error;
}
}
finally
{
EndPresentationOnlySpatialMutation(key);
_rebucketingGuid = priorRebucketingGuid;
}
if (!IsCurrentProjectionOperation(
record.ServerGuid,
record,
projectionOperation)
|| !TryGetRuntimePlacementProjectionRecord(
token,
requirePlacementVersions: false,
out LiveEntityRecord? current)
|| !ReferenceEquals(current, record)
|| !ReferenceEquals(current.WorldEntity, entity))
{
ThrowAfterCommittedProjectionChange(
record.ServerGuid,
spatialNotificationFailure,
runtimeNotificationFailure: null);
return false;
}
record.IsSpatiallyVisible = false;
RefreshSpatialPresentationIndexes(record);
RefreshPresentation(record);
if (!IsCurrentProjectionOperation(
record.ServerGuid,
record,
projectionOperation))
{
ThrowAfterCommittedProjectionChange(
record.ServerGuid,
spatialNotificationFailure,
runtimeNotificationFailure: null);
return false;
}
ThrowAfterCommittedProjectionChange(
record.ServerGuid,
spatialNotificationFailure,
runtimeNotificationFailure: null);
return true;
}
private bool TryGetRuntimePlacementProjectionRecord(
in RuntimePlacementProjectionToken token,
bool requirePlacementVersions,
out LiveEntityRecord record)
{
if (!token.IsValid
|| token.SessionLifetimeVersion != _directory.SessionLifetimeVersion
|| !_projections.TryGet(token.Entity, out record!)
|| !_directory.IsCurrent(record.Canonical)
|| record.Canonical.Key != token.Entity
|| RequireProjectionKey(record) != token.Entity
|| !IsValidPortalPlacementAuthority(token))
{
record = null!;
return false;
}
if (requirePlacementVersions
&& (record.Canonical.PositionAuthorityVersion
!= token.PositionAuthorityVersion
|| record.Canonical.SpatialAuthorityVersion
!= token.SpatialAuthorityVersion
|| record.Canonical.PlacementCommitVersion
!= token.PlacementCommitVersion
|| record.Canonical.FullCellId != token.ExactCellId))
{
record = null!;
return false;
}
return true;
}
private static bool IsValidPortalPlacementAuthority(
in RuntimePlacementProjectionToken token)
{
RuntimePortalPlacementAuthority portal = token.Portal;
if (!portal.Present)
return portal.IsEmpty;
return portal.IsValid
&& portal.Projection.DestinationCell == token.ExactCellId;
}
private void BeginPresentationOnlySpatialMutation(RuntimeEntityKey key)
{
_presentationOnlySpatialMutationDepth.TryGetValue(key, out int depth);
_presentationOnlySpatialMutationDepth[key] = checked(depth + 1);
}
private void EndPresentationOnlySpatialMutation(RuntimeEntityKey key)
{
if (!_presentationOnlySpatialMutationDepth.TryGetValue(
key,
out int depth)
|| depth <= 0)
{
throw new InvalidOperationException(
"Presentation-only spatial mutation depth was not balanced.");
}
if (depth == 1)
_presentationOnlySpatialMutationDepth.Remove(key);
else
_presentationOnlySpatialMutationDepth[key] = depth - 1;
}
/// <summary>
/// Removes only the render-bucket reference. The logical record and every
/// create-time resource remain alive for later projection/rebucketing.
@ -2588,6 +2862,33 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
_physics.AcknowledgeSpatialProjection(record.Canonical, spatial);
RefreshSpatialPresentationIndexes(record, current, spatial, key);
}
private void RefreshSpatialPresentationIndexes(LiveEntityRecord record)
{
bool current = IsCurrentRecord(record);
bool spatial = current && HasSpatialRuntimeProjection(record);
if (record.ProjectionKey is not { } key)
{
if (record.WorldEntity is not null)
{
throw new InvalidOperationException(
$"Materialized live entity 0x{record.ServerGuid:X8}/{record.Generation} has no exact projection key.");
}
return;
}
RefreshSpatialPresentationIndexes(record, current, spatial, key);
}
private void RefreshSpatialPresentationIndexes(
LiveEntityRecord record,
bool current,
bool spatial,
RuntimeEntityKey key)
{
if (record.WorldEntity is not null)
{
if (spatial && record.AnimationRuntime is { } animation)
@ -2641,6 +2942,13 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
bool wasVisible = record.IsSpatiallyVisible;
if (RequireProjectionKey(record) != key)
return;
if (_presentationOnlySpatialMutationDepth.ContainsKey(key))
{
record.IsSpatiallyVisible = visible;
RefreshSpatialPresentationIndexes(record);
RefreshPresentation(record);
return;
}
bool wasOrdinaryRoot = _physics.IsSpatialRoot(record.Canonical);
record.IsSpatiallyVisible = visible;
bool isOrdinaryRoot = record.ProjectionKind is LiveEntityProjectionKind.World

View file

@ -0,0 +1,175 @@
using AcDream.Runtime.Physics;
using AcDream.Runtime.World;
using AcDream.App.Physics;
using AcDream.App.Rendering.Vfx;
using AcDream.Core.Plugins;
using AcDream.Core.World;
using AcDream.Plugin.Abstractions;
namespace AcDream.App.World;
/// <summary>
/// Graphical projection sink for canonical Runtime SetPosition receipts. It
/// owns only App-facing world, effect-pose, cached-local-shadow, selection,
/// and renderer/VFX visibility projections. Runtime physics, shadows, body
/// state, clocks, and worksets were committed before this sink is invoked.
///
/// This adapter deliberately owns no subscription: production composition
/// activates the shared observer only after both graphical and no-window hosts
/// implement the same presentation-only contract.
/// </summary>
internal sealed class RuntimePlacementPresentationSink
: IRuntimePlacementProjectionSink
{
private readonly LiveEntityRuntime _liveEntities;
private readonly RuntimeWorldTransitState _transit;
private readonly WorldGameState _worldState;
private readonly WorldEvents _worldEvents;
private readonly EntityEffectPoseRegistry _effectPoses;
private readonly LocalPlayerShadowState _localPlayerShadow;
private readonly Func<uint> _localPlayerGuid;
private readonly Action<uint> _clearSelectionForUnavailableEntity;
private readonly Action<LiveEntityRecord, bool>[] _visibilitySinks;
public RuntimePlacementPresentationSink(
LiveEntityRuntime liveEntities,
RuntimeWorldTransitState transit,
WorldGameState worldState,
WorldEvents worldEvents,
EntityEffectPoseRegistry effectPoses,
LocalPlayerShadowState localPlayerShadow,
Func<uint> localPlayerGuid,
Action<uint> clearSelectionForUnavailableEntity,
IEnumerable<Action<LiveEntityRecord, bool>>? visibilitySinks = null)
{
_liveEntities = liveEntities
?? throw new ArgumentNullException(nameof(liveEntities));
_transit = transit ?? throw new ArgumentNullException(nameof(transit));
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
_worldEvents = worldEvents ?? throw new ArgumentNullException(nameof(worldEvents));
_effectPoses = effectPoses
?? throw new ArgumentNullException(nameof(effectPoses));
_localPlayerShadow = localPlayerShadow
?? throw new ArgumentNullException(nameof(localPlayerShadow));
_localPlayerGuid = localPlayerGuid
?? throw new ArgumentNullException(nameof(localPlayerGuid));
_clearSelectionForUnavailableEntity = clearSelectionForUnavailableEntity
?? throw new ArgumentNullException(
nameof(clearSelectionForUnavailableEntity));
_visibilitySinks = visibilitySinks?.ToArray()
?? Array.Empty<Action<LiveEntityRecord, bool>>();
if (_visibilitySinks.Any(static sink => sink is null))
throw new ArgumentException(
"Presentation visibility sinks cannot contain null.",
nameof(visibilitySinks));
}
public bool TryApply(in RuntimePlacementProjectionSnapshot projection)
{
if (projection.Kind is RuntimePlacementProjectionKind.Place
&& !_transit.IsCurrentPlacementAuthority(
projection.Token.Portal,
projection.Token.ExactCellId))
{
return false;
}
if (!_liveEntities.TryApplyRuntimePlacementProjection(in projection))
return false;
if (projection.Kind is RuntimePlacementProjectionKind.Discard)
return true;
if (!_liveEntities.TryGetRecord(
projection.Token.Entity,
out LiveEntityRecord record)
|| record.WorldEntity is not { } entity)
{
return false;
}
return projection.Kind switch
{
RuntimePlacementProjectionKind.Place =>
TryPublishPlace(record, entity),
RuntimePlacementProjectionKind.Withdraw =>
TryPublishWithdrawal(record, entity),
_ => false,
};
}
private bool TryPublishPlace(LiveEntityRecord record, WorldEntity entity)
{
if (!IsCurrent(record, entity))
return false;
WorldEntitySnapshot snapshot = Snapshot(entity);
_worldState.Add(snapshot);
if (!IsCurrent(record, entity))
return false;
_worldEvents.UpsertCurrent(snapshot);
if (!IsCurrent(record, entity))
return false;
_effectPoses.PublishMeshRefs(entity);
if (!IsCurrent(record, entity))
return false;
if (record.ServerGuid == _localPlayerGuid())
{
_localPlayerShadow.Set(
entity.Position,
entity.Rotation,
record.FullCellId);
}
for (int i = 0; i < _visibilitySinks.Length; i++)
{
_visibilitySinks[i](record, true);
if (!IsCurrent(record, entity))
return false;
}
return true;
}
private bool TryPublishWithdrawal(
LiveEntityRecord record,
WorldEntity entity)
{
if (!IsCurrent(record, entity))
return false;
for (int i = 0; i < _visibilitySinks.Length; i++)
{
_visibilitySinks[i](record, false);
if (!IsCurrent(record, entity))
return false;
}
_worldState.RemoveById(entity.Id);
if (!IsCurrent(record, entity))
return false;
_worldEvents.ForgetEntity(entity.Id);
if (!IsCurrent(record, entity))
return false;
_effectPoses.Remove(entity.Id);
if (!IsCurrent(record, entity))
return false;
if (record.ServerGuid == _localPlayerGuid())
_localPlayerShadow.Clear();
if (!IsCurrent(record, entity))
return false;
_clearSelectionForUnavailableEntity(record.ServerGuid);
return IsCurrent(record, entity);
}
private bool IsCurrent(LiveEntityRecord record, WorldEntity entity) =>
_liveEntities.TryGetRecord(
record.ProjectionKey!.Value,
out LiveEntityRecord current)
&& ReferenceEquals(current, record)
&& ReferenceEquals(current.WorldEntity, entity);
private static WorldEntitySnapshot Snapshot(WorldEntity entity) => new(
entity.Id,
entity.SourceGfxObjOrSetupId,
entity.Position,
entity.Rotation);
}

View file

@ -1,4 +1,5 @@
using System.Globalization;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.World;
@ -247,6 +248,32 @@ public sealed class RuntimeWorldTransitState
return false;
}
/// <summary>
/// Validates the portal suffix carried by a canonical SetPosition
/// projection receipt against this instance's live transit ownership.
/// Shape equality alone is insufficient: a superseded, cancelled, or
/// completed host token can remain structurally valid after it has lost
/// authority to reveal the destination.
/// </summary>
public bool IsCurrentPlacementAuthority(
in RuntimePortalPlacementAuthority authority,
uint exactCellId)
{
if (!authority.Present)
return authority.IsEmpty;
return authority.IsValid
&& authority.Projection.DestinationCell == exactCellId
&& IsCurrentPortalDestination(
authority.RevealGeneration,
authority.TeleportSequence,
exactCellId)
&& TryGetHostProjection(
authority.Projection,
out RuntimeWorldHostProjectionSnapshot host)
&& !host.IsSuperseding;
}
/// <summary>
/// Marks the graphical reservation-release callback as due before the
/// host executes it. A thrown callback therefore leaves an exact pending

View file

@ -0,0 +1,813 @@
using System.Numerics;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.App.Physics;
using AcDream.App.Rendering.Vfx;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Plugins;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
using AcDream.Runtime.World;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.World;
public sealed class RuntimePlacementPresentationSinkTests
{
private const uint SourceCell = 0x01010001u;
private const uint DestinationCell = 0x01020001u;
private const uint Guid = 0x7000A101u;
[Fact]
public void Place_ReframesAndRebucketsExactSidecarWithoutMutatingRuntimePhysics()
{
Fixture fixture = Fixture.Create(twoLandblocks: true);
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
// Model the already-committed canonical SetPosition edge whose receipt
// the dormant graphical observer is now projecting.
record.FullCellId = DestinationCell;
record.CanonicalLandblockId =
(DestinationCell & 0xFFFF0000u) | 0xFFFFu;
record.Canonical.AdvancePlacementCommit();
RuntimePlacementProjectionSnapshot place = Placement(
fixture,
record,
RuntimePlacementProjectionKind.Place,
new Vector3(44f, 55f, 66f),
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f));
RuntimeOwnershipSnapshot before = RuntimeOwnershipSnapshot.Capture(
fixture.Runtime,
record);
int genericVisibilityCount = 0;
fixture.Runtime.ProjectionVisibilityChanged += (_, _) =>
genericVisibilityCount++;
Assert.True(fixture.Sink.TryApply(in place));
Assert.Equal(place.WorldPosition, entity.Position);
Assert.Equal(place.Orientation, entity.Rotation);
Assert.Equal(DestinationCell, entity.ParentCellId);
Assert.True(record.IsSpatiallyProjected);
Assert.True(record.IsSpatiallyVisible);
Assert.Contains(record, fixture.Runtime.VisibleRecords);
Assert.True(fixture.Spatial.IsLiveEntityProjectionResident(
record.ProjectionKey!.Value));
Assert.Equal(before, RuntimeOwnershipSnapshot.Capture(
fixture.Runtime,
record));
Assert.Equal(0, genericVisibilityCount);
Assert.Equal(
place.WorldPosition,
Assert.Single(fixture.WorldState.Entities).Position);
Assert.Equal((record, true), Assert.Single(fixture.Visibility));
Assert.Equal(
new LocalPlayerShadowState.Snapshot(
place.WorldPosition,
place.Orientation,
DestinationCell),
fixture.LocalShadow.Current);
fixture.Spatial.RemoveLandblock(SourceCell | 0xFFFFu);
Assert.True(fixture.Spatial.IsLiveEntityProjectionResident(
record.ProjectionKey.Value));
}
[Fact]
public void Withdraw_RemovesOnlyPresentationAndRetainsLogicalRuntimeOwnership()
{
Fixture fixture = Fixture.Create();
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
RuntimePlacementProjectionSnapshot withdraw = Placement(
fixture,
record,
RuntimePlacementProjectionKind.Withdraw,
record.WorldEntity!.Position,
record.WorldEntity.Rotation);
RuntimeOwnershipSnapshot before = RuntimeOwnershipSnapshot.Capture(
fixture.Runtime,
record);
int genericVisibilityCount = 0;
fixture.Runtime.ProjectionVisibilityChanged += (_, _) =>
genericVisibilityCount++;
Assert.True(fixture.Sink.TryApply(in withdraw));
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord current));
Assert.Same(record, current);
Assert.NotNull(record.WorldEntity);
Assert.True(record.ResourcesRegistered);
Assert.False(record.IsSpatiallyProjected);
Assert.False(record.IsSpatiallyVisible);
Assert.DoesNotContain(record, fixture.Runtime.VisibleRecords);
Assert.False(fixture.Spatial.IsLiveEntityProjectionResident(
record.ProjectionKey!.Value));
Assert.Equal(before, RuntimeOwnershipSnapshot.Capture(
fixture.Runtime,
record));
Assert.Equal(0, genericVisibilityCount);
Assert.Empty(fixture.WorldState.Entities);
Assert.Equal(0, fixture.EffectPoses.Count);
Assert.Null(fixture.LocalShadow.Current);
Assert.Equal((record, false), Assert.Single(fixture.Visibility));
Assert.Equal(Guid, Assert.Single(fixture.ClearedSelection));
}
[Fact]
public void Discard_IsAckOnlyNoOpEvenWhenTokenIsStaleOrSidecarIsGone()
{
Fixture fixture = Fixture.Create();
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
WorldEntity entity = record.WorldEntity!;
RuntimePlacementProjectionSnapshot discard = Placement(
fixture,
record,
RuntimePlacementProjectionKind.Discard,
new Vector3(900f),
Quaternion.CreateFromAxisAngle(Vector3.UnitX, 1f)) with
{
Token = Placement(fixture, record,
RuntimePlacementProjectionKind.Place,
Vector3.Zero,
Quaternion.Identity).Token with
{
SessionLifetimeVersion = ulong.MaxValue,
PositionAuthorityVersion = ulong.MaxValue,
ExactCellId = 0xDEAD0001u,
},
};
Vector3 priorPosition = entity.Position;
Quaternion priorRotation = entity.Rotation;
bool priorVisible = record.IsSpatiallyVisible;
Assert.True(fixture.Sink.TryApply(in discard));
Assert.Equal(priorPosition, entity.Position);
Assert.Equal(priorRotation, entity.Rotation);
Assert.Equal(priorVisible, record.IsSpatiallyVisible);
}
[Fact]
public void Place_RejectsStaleCanonicalVersionsWithoutChangingSidecar()
{
Fixture fixture = Fixture.Create();
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
WorldEntity entity = record.WorldEntity!;
RuntimePlacementProjectionSnapshot stale = Placement(
fixture,
record,
RuntimePlacementProjectionKind.Place,
new Vector3(90f, 91f, 92f),
Quaternion.CreateFromAxisAngle(Vector3.UnitY, 1f));
record.Canonical.AdvancePlacementCommit();
Vector3 priorPosition = entity.Position;
Quaternion priorRotation = entity.Rotation;
Assert.False(fixture.Sink.TryApply(in stale));
Assert.Equal(priorPosition, entity.Position);
Assert.Equal(priorRotation, entity.Rotation);
Assert.True(record.IsSpatiallyVisible);
}
[Fact]
public void Place_WithoutMaterializedSidecarRemainsPendingForRetry()
{
Fixture fixture = Fixture.Create();
LiveEntityRegistrationResult registration = fixture.Runtime.RegisterLiveEntity(
Spawn(Guid, 1, SourceCell));
RuntimeEntityRecord canonical = Assert.IsType<RuntimeEntityRecord>(
registration.Canonical);
RuntimeEntityKey missingKey = new(0x60000042u, canonical.Incarnation);
var token = new RuntimePlacementProjectionToken(
Sequence: 1,
Revision: 1,
Entity: missingKey,
PositionAuthorityVersion: canonical.PositionAuthorityVersion,
SpatialAuthorityVersion: canonical.SpatialAuthorityVersion,
PlacementCommitVersion: canonical.PlacementCommitVersion,
SessionLifetimeVersion: fixture.Runtime.SessionLifetimeVersion,
ExactCellId: canonical.FullCellId,
CollisionGeneration: 1,
Portal: default);
var place = new RuntimePlacementProjectionSnapshot(
token,
RuntimePlacementProjectionKind.Place,
new Vector3(1f, 2f, 3f),
Quaternion.Identity,
Vector3.Zero,
InContact: false,
OnWalkable: false);
Assert.False(fixture.Sink.TryApply(in place));
Assert.Empty(fixture.Spatial.Entities);
}
[Fact]
public void Place_WithoutLoadedDestinationBackendRemainsPendingAtPriorProjection()
{
Fixture fixture = Fixture.Create();
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
WorldEntity entity = record.WorldEntity!;
Vector3 priorPosition = entity.Position;
record.FullCellId = DestinationCell;
record.CanonicalLandblockId =
(DestinationCell & 0xFFFF0000u) | 0xFFFFu;
record.Canonical.AdvancePlacementCommit();
RuntimePlacementProjectionSnapshot place = Placement(
fixture,
record,
RuntimePlacementProjectionKind.Place,
new Vector3(70f, 71f, 72f),
Quaternion.Identity);
Assert.False(fixture.Sink.TryApply(in place));
Assert.Equal(priorPosition, entity.Position);
Assert.True(record.IsSpatiallyVisible);
Assert.True(fixture.Spatial.IsLiveEntityProjectionResident(
record.ProjectionKey!.Value));
Assert.Empty(fixture.Visibility);
}
[Fact]
public void Place_UsesExactIncarnationAndCannotMutateSameGuidReplacement()
{
Fixture fixture = Fixture.Create();
LiveEntityRecord old = fixture.Materialize(Spawn(Guid, 1, SourceCell));
RuntimePlacementProjectionSnapshot stale = Placement(
fixture,
old,
RuntimePlacementProjectionKind.Place,
new Vector3(88f, 77f, 66f),
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 1.25f));
LiveEntityRecord replacement = fixture.Materialize(
Spawn(Guid, 2, SourceCell));
WorldEntity replacementEntity = replacement.WorldEntity!;
Vector3 priorPosition = replacementEntity.Position;
Quaternion priorRotation = replacementEntity.Rotation;
Assert.False(fixture.Sink.TryApply(in stale));
Assert.Equal(priorPosition, replacementEntity.Position);
Assert.Equal(priorRotation, replacementEntity.Rotation);
Assert.True(replacement.IsSpatiallyVisible);
}
[Fact]
public void PortalPlace_RequiresExactCurrentTransitHostAndSequence()
{
Fixture fixture = Fixture.Create();
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
RuntimePlacementProjectionSnapshot ordinary = Placement(
fixture,
record,
RuntimePlacementProjectionKind.Place,
new Vector3(21f, 22f, 23f),
Quaternion.Identity);
RuntimePortalPlacementAuthority authority = fixture.BeginPortal(
SourceCell,
teleportSequence: 9);
RuntimePlacementProjectionSnapshot current = ordinary with
{
Token = ordinary.Token with { Portal = authority },
};
Assert.True(fixture.Sink.TryApply(in current));
Assert.Equal(current.WorldPosition, record.WorldEntity!.Position);
Assert.True(fixture.Transit.BeginHostProjectionSupersession(
authority.Projection));
RuntimePlacementProjectionSnapshot superseded = current with
{
WorldPosition = new Vector3(80f, 81f, 82f),
};
Assert.False(fixture.Sink.TryApply(in superseded));
Assert.Equal(current.WorldPosition, record.WorldEntity.Position);
RuntimePlacementProjectionSnapshot wrongSequence = current with
{
Token = current.Token with
{
Portal = authority with { TeleportSequence = 10 },
},
WorldPosition = new Vector3(90f, 91f, 92f),
};
Assert.False(fixture.Sink.TryApply(in wrongSequence));
Assert.Equal(current.WorldPosition, record.WorldEntity.Position);
}
[Fact]
public void PlaceAndWithdraw_DoNotMutateRemoteBodyOrRuntimeOwnership()
{
Fixture fixture = Fixture.Create();
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
RemoteMotion remote = fixture.Runtime.GetOrCreateRemoteMotionRuntime(Guid);
remote.Body.Position = new Vector3(4f, 5f, 6f);
remote.Body.Orientation = Quaternion.CreateFromAxisAngle(
Vector3.UnitY,
0.4f);
remote.Body.State = PhysicsStateFlags.Gravity
| PhysicsStateFlags.ReportCollisions;
remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
remote.Body.InWorld = true;
remote.Body.LastUpdateTime = 42.5;
PhysicsBodySnapshot bodyBefore = PhysicsBodySnapshot.Capture(remote.Body);
RuntimePhysicsOwnershipSnapshot runtimeBefore =
fixture.Runtime.Physics.CaptureOwnership();
RuntimePlacementProjectionSnapshot place = Placement(
fixture,
record,
RuntimePlacementProjectionKind.Place,
new Vector3(30f, 31f, 32f),
Quaternion.Identity);
Assert.True(fixture.Sink.TryApply(in place));
Assert.Equal(bodyBefore, PhysicsBodySnapshot.Capture(remote.Body));
Assert.Equal(runtimeBefore, fixture.Runtime.Physics.CaptureOwnership());
RuntimePlacementProjectionSnapshot withdraw = place with
{
Kind = RuntimePlacementProjectionKind.Withdraw,
};
Assert.True(fixture.Sink.TryApply(in withdraw));
Assert.Equal(bodyBefore, PhysicsBodySnapshot.Capture(remote.Body));
Assert.Equal(runtimeBefore, fixture.Runtime.Physics.CaptureOwnership());
}
[Fact]
public void Withdraw_DoesNotMutateProjectileBodyShadowOrWorksetOwnership()
{
Fixture fixture = Fixture.Create();
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
PhysicsBody body = fixture.Runtime.GetOrCreatePhysicsBody(
Guid,
_ => new PhysicsBody());
body.Position = new Vector3(7f, 8f, 9f);
body.Orientation = Quaternion.CreateFromAxisAngle(Vector3.UnitX, 0.6f);
body.State = PhysicsStateFlags.Missile
| PhysicsStateFlags.ReportCollisions;
body.TransientState = TransientStateFlags.Active;
body.InWorld = true;
body.LastUpdateTime = 99.25;
fixture.Runtime.BindProjectileRuntime(
Guid,
body,
new ProjectileCollisionSphere(Vector3.Zero, 0.25f));
PhysicsBodySnapshot bodyBefore = PhysicsBodySnapshot.Capture(body);
RuntimePhysicsOwnershipSnapshot runtimeBefore =
fixture.Runtime.Physics.CaptureOwnership();
RuntimePlacementProjectionSnapshot withdraw = Placement(
fixture,
record,
RuntimePlacementProjectionKind.Withdraw,
record.WorldEntity!.Position,
record.WorldEntity.Rotation);
Assert.True(fixture.Sink.TryApply(in withdraw));
Assert.Equal(bodyBefore, PhysicsBodySnapshot.Capture(body));
Assert.Equal(runtimeBefore, fixture.Runtime.Physics.CaptureOwnership());
Assert.Same(body, record.ProjectileRuntime!.Body);
}
[Fact]
public void FailedPresentationTail_RetriesSameReceiptIdempotently()
{
using var fixture = SubscriptionFixture.Create();
fixture.VisibilityFailuresRemaining = 1;
using var subscription = new RuntimePlacementProjectionSubscription(
fixture.Lifetime.Placements,
() => fixture.Generation,
fixture.Sink);
Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
fixture.Record.Canonical,
publishWithdrawal: true));
Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
Assert.Equal(1, fixture.Lifetime.Events.DispatchFailureCount);
Assert.IsType<InvalidOperationException>(
fixture.Lifetime.Events.LastDispatchFailure);
Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
Assert.True(subscription.RetryPending());
Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
Assert.False(fixture.Record.IsSpatiallyProjected);
Assert.Empty(fixture.WorldState.Entities);
Assert.Equal(0, fixture.EffectPoses.Count);
Assert.Equal(2, fixture.Visibility.Count);
Assert.False(subscription.HasAppliedReceiptAwaitingAcknowledgement);
}
private static RuntimePlacementProjectionSnapshot Placement(
Fixture fixture,
LiveEntityRecord record,
RuntimePlacementProjectionKind kind,
Vector3 position,
Quaternion orientation)
{
RuntimeEntityRecord canonical = record.Canonical;
var token = new RuntimePlacementProjectionToken(
Sequence: 1,
Revision: 1,
Entity: record.ProjectionKey!.Value,
PositionAuthorityVersion: canonical.PositionAuthorityVersion,
SpatialAuthorityVersion: canonical.SpatialAuthorityVersion,
PlacementCommitVersion: canonical.PlacementCommitVersion,
SessionLifetimeVersion: fixture.Runtime.SessionLifetimeVersion,
ExactCellId: canonical.FullCellId,
CollisionGeneration: 1,
Portal: default);
return new RuntimePlacementProjectionSnapshot(
token,
kind,
position,
orientation,
CellLocalPosition: position,
InContact: false,
OnWalkable: false);
}
private static WorldSession.EntitySpawn Spawn(
uint guid,
ushort instance,
uint cell)
{
var position = new CreateObject.ServerPosition(
cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: instance);
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.ReportCollisions,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
position,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
null,
null,
"fixture",
null,
null,
0x09000001u,
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
InstanceSequence: instance,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
private readonly record struct RuntimeOwnershipSnapshot(
ulong PositionAuthorityVersion,
ulong SpatialAuthorityVersion,
ulong PlacementCommitVersion,
ulong ObjectClockEpoch,
bool ObjectClockIsActive,
int SpatialRootCount,
int SpatialRemoteCount,
int SpatialProjectileCount,
RuntimePhysicsOwnershipSnapshot Physics)
{
internal static RuntimeOwnershipSnapshot Capture(
LiveEntityRuntime runtime,
LiveEntityRecord record) => new(
record.Canonical.PositionAuthorityVersion,
record.Canonical.SpatialAuthorityVersion,
record.Canonical.PlacementCommitVersion,
record.ObjectClockEpoch,
record.ObjectClock.IsActive,
runtime.SpatialRootObjectCount,
runtime.SpatialRemoteMotionRuntimeCount,
runtime.SpatialProjectileRuntimeCount,
runtime.Physics.CaptureOwnership());
}
private readonly record struct PhysicsBodySnapshot(
Vector3 Position,
Quaternion Orientation,
PhysicsStateFlags State,
TransientStateFlags TransientState,
bool InWorld,
double LastUpdateTime)
{
internal static PhysicsBodySnapshot Capture(PhysicsBody body) => new(
body.Position,
body.Orientation,
body.State,
body.TransientState,
body.InWorld,
body.LastUpdateTime);
}
private sealed class Fixture
{
private Fixture(
GpuWorldState spatial,
LiveEntityRuntime runtime,
RuntimeWorldTransitState transit,
WorldGameState worldState,
WorldEvents worldEvents,
EntityEffectPoseRegistry effectPoses,
LocalPlayerShadowState localShadow)
{
Spatial = spatial;
Runtime = runtime;
Transit = transit;
WorldState = worldState;
WorldEvents = worldEvents;
EffectPoses = effectPoses;
LocalShadow = localShadow;
Sink = new RuntimePlacementPresentationSink(
runtime,
transit,
worldState,
worldEvents,
effectPoses,
localShadow,
() => Guid,
ClearedSelection.Add,
[
(record, visible) =>
{
Visibility.Add((record, visible));
if (VisibilityFailuresRemaining > 0)
{
VisibilityFailuresRemaining--;
throw new InvalidOperationException(
"fixture presentation failure");
}
},
]);
}
internal GpuWorldState Spatial { get; }
internal LiveEntityRuntime Runtime { get; }
internal RuntimeWorldTransitState Transit { get; }
internal WorldGameState WorldState { get; }
internal WorldEvents WorldEvents { get; }
internal EntityEffectPoseRegistry EffectPoses { get; }
internal LocalPlayerShadowState LocalShadow { get; }
internal List<(LiveEntityRecord Record, bool Visible)> Visibility { get; } = [];
internal List<uint> ClearedSelection { get; } = [];
internal int VisibilityFailuresRemaining { get; set; }
internal RuntimePlacementPresentationSink Sink { get; }
internal static Fixture Create(bool twoLandblocks = false)
{
var spatial = new GpuWorldState();
spatial.AddLandblock(EmptyLandblock(SourceCell | 0xFFFFu));
if (twoLandblocks)
spatial.AddLandblock(EmptyLandblock(DestinationCell | 0xFFFFu));
var resources = new RecordingResources();
LiveEntityRuntime runtime = LiveEntityRuntimeFixture.Create(
spatial,
resources);
return new Fixture(
spatial,
runtime,
new RuntimeWorldTransitState(),
new WorldGameState(),
new WorldEvents(),
new EntityEffectPoseRegistry(),
new LocalPlayerShadowState());
}
internal LiveEntityRecord Materialize(WorldSession.EntitySpawn spawn)
{
LiveEntityRecord record = Runtime.RegisterAndMaterializeProjection(spawn);
Assert.True(record.ResourcesRegistered);
WorldEntity entity = record.WorldEntity!;
var snapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot(
entity.Id,
entity.SourceGfxObjOrSetupId,
entity.Position,
entity.Rotation);
WorldState.Add(snapshot);
WorldEvents.UpsertCurrent(snapshot);
EffectPoses.PublishMeshRefs(entity);
if (record.ServerGuid == Guid)
{
LocalShadow.Set(
entity.Position,
entity.Rotation,
record.FullCellId);
}
return record;
}
internal RuntimePortalPlacementAuthority BeginPortal(
uint cell,
ushort teleportSequence)
{
Assert.True(Transit.TryQueueTeleportStart(teleportSequence));
Assert.True(Transit.ActivateQueuedTeleport());
Assert.True(Transit.OfferTeleportDestination(
new RuntimeTeleportDestination(
Guid,
InstanceSequence: 1,
PositionSequence: 1,
TeleportSequence: teleportSequence,
ForcePositionSequence: 1,
new Position(
cell,
new Vector3(1f, 2f, 3f),
Quaternion.Identity)),
teleportTimestampAdvanced: true));
Assert.True(Transit.TryBeginPortalReveal(
teleportSequence,
cell,
out long generation));
Assert.True(Transit.TryRegisterHostProjection(
generation,
cell,
out RuntimeWorldHostProjectionToken host));
return new RuntimePortalPlacementAuthority(
true,
generation,
teleportSequence,
host);
}
private static LoadedLandblock EmptyLandblock(uint canonicalId) =>
new(canonicalId, new LandBlock(), Array.Empty<WorldEntity>());
}
private sealed class SubscriptionFixture : IDisposable
{
private SubscriptionFixture(
RuntimeEntityObjectLifetime lifetime,
LiveEntityRuntime runtime,
LiveEntityRecord record,
RuntimePlacementPresentationSink sink,
WorldGameState worldState,
EntityEffectPoseRegistry effectPoses,
List<(LiveEntityRecord Record, bool Visible)> visibility)
{
Lifetime = lifetime;
Runtime = runtime;
Record = record;
Sink = sink;
WorldState = worldState;
EffectPoses = effectPoses;
Visibility = visibility;
}
internal RuntimeGenerationToken Generation { get; } = new(7UL);
internal RuntimeEntityObjectLifetime Lifetime { get; }
internal LiveEntityRuntime Runtime { get; }
internal LiveEntityRecord Record { get; }
internal RuntimePlacementPresentationSink Sink { get; }
internal WorldGameState WorldState { get; }
internal EntityEffectPoseRegistry EffectPoses { get; }
internal List<(LiveEntityRecord Record, bool Visible)> Visibility { get; }
internal int VisibilityFailuresRemaining { get; set; }
internal static SubscriptionFixture Create()
{
PhysicsEngine engine = FlatEngine();
var lifetime = new RuntimeEntityObjectLifetime(engine);
RuntimeGenerationToken generation = new(7UL);
lifetime.BindEventContext(() => generation, static () => 11UL);
var spatial = new GpuWorldState();
spatial.AddLandblock(new LoadedLandblock(
SourceCell | 0xFFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
var runtime = new LiveEntityRuntime(
spatial,
new RecordingResources(),
lifetime);
LiveEntityRecord record = runtime.RegisterAndMaterializeProjection(
Spawn(Guid, 1, SourceCell));
PhysicsBody body = runtime.GetOrCreatePhysicsBody(
Guid,
_ => new PhysicsBody
{
Position = new Vector3(10f, 10f, 5f),
Orientation = Quaternion.Identity,
LastUpdateTime = 1d,
State = PhysicsStateFlags.ReportCollisions,
TransientState = TransientStateFlags.Active,
});
body.SnapToCell(
SourceCell,
body.Position,
body.Position);
var worldState = new WorldGameState();
var worldEvents = new WorldEvents();
var effectPoses = new EntityEffectPoseRegistry();
var localShadow = new LocalPlayerShadowState();
WorldEntity entity = record.WorldEntity!;
var snapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot(
entity.Id,
entity.SourceGfxObjOrSetupId,
entity.Position,
entity.Rotation);
worldState.Add(snapshot);
worldEvents.UpsertCurrent(snapshot);
effectPoses.PublishMeshRefs(entity);
localShadow.Set(entity.Position, entity.Rotation, record.FullCellId);
var visibility = new List<(LiveEntityRecord Record, bool Visible)>();
SubscriptionFixture? fixture = null;
var sink = new RuntimePlacementPresentationSink(
runtime,
new RuntimeWorldTransitState(),
worldState,
worldEvents,
effectPoses,
localShadow,
() => Guid,
_ => { },
[
(candidate, visible) =>
{
visibility.Add((candidate, visible));
if (fixture!.VisibilityFailuresRemaining > 0)
{
fixture.VisibilityFailuresRemaining--;
throw new InvalidOperationException(
"fixture presentation failure");
}
},
]);
fixture = new SubscriptionFixture(
lifetime,
runtime,
record,
sink,
worldState,
effectPoses,
visibility);
return fixture;
}
public void Dispose()
{
Runtime.Clear();
Lifetime.Dispose();
}
private static PhysicsEngine FlatEngine()
{
var engine = new PhysicsEngine
{
DataCache = new PhysicsDataCache(),
};
engine.AddLandblock(
SourceCell & 0xFFFF0000u,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
return engine;
}
}
private sealed class RecordingResources : ILiveEntityResourceLifecycle
{
public void Register(WorldEntity entity) { }
public void Unregister(WorldEntity entity) { }
}
}

View file

@ -1,5 +1,6 @@
using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Runtime.Physics;
using AcDream.Runtime.World;
namespace AcDream.Runtime.Tests.World;
@ -9,6 +10,77 @@ public sealed class RuntimeWorldTransitStateTests
private const uint OutdoorCell = 0x11340021u;
private const uint OtherCell = 0x3032001Cu;
[Fact]
public void PlacementAuthority_RequiresCurrentPortalHostSequenceAndReveal()
{
var state = new RuntimeWorldTransitState();
long generation = BeginPortal(state, OutdoorCell, sequence: 7);
RuntimeWorldHostProjectionToken host =
RegisterHost(state, generation, OutdoorCell);
RuntimePortalPlacementAuthority authority = new(
Present: true,
RevealGeneration: generation,
TeleportSequence: 7,
Projection: host);
Assert.True(state.IsCurrentPlacementAuthority(
authority,
OutdoorCell));
Assert.True(state.IsCurrentPlacementAuthority(default, OutdoorCell));
Assert.False(state.IsCurrentPlacementAuthority(
authority with { TeleportSequence = 8 },
OutdoorCell));
Assert.False(state.IsCurrentPlacementAuthority(
authority,
OtherCell));
Assert.True(state.BeginHostProjectionSupersession(host));
Assert.False(state.IsCurrentPlacementAuthority(
authority,
OutdoorCell));
}
[Fact]
public void PlacementAuthority_RejectsCancelledAndCompletedReveals()
{
var cancelled = new RuntimeWorldTransitState();
long cancelledGeneration = BeginPortal(cancelled, OutdoorCell);
RuntimeWorldHostProjectionToken cancelledHost =
RegisterHost(cancelled, cancelledGeneration, OutdoorCell);
RuntimePortalPlacementAuthority cancelledAuthority = new(
true,
cancelledGeneration,
1,
cancelledHost);
Assert.True(cancelled.Cancel(cancelledGeneration));
Assert.False(cancelled.IsCurrentPlacementAuthority(
cancelledAuthority,
OutdoorCell));
var completed = new RuntimeWorldTransitState();
long completedGeneration = BeginPortal(completed, OutdoorCell);
RuntimeWorldHostProjectionToken completedHost =
RegisterHost(completed, completedGeneration, OutdoorCell);
RuntimePortalPlacementAuthority completedAuthority = new(
true,
completedGeneration,
1,
completedHost);
Assert.True(completed.AcknowledgeDestinationReadiness(
Ready(completedGeneration, OutdoorCell)));
Assert.True(completed.AcknowledgePortalMaterialized(
completedGeneration,
1,
OutdoorCell));
Assert.True(completed.AcknowledgeWorldViewportVisible(
completedGeneration));
Assert.True(completed.Complete(completedGeneration));
Assert.False(completed.IsCurrentPlacementAuthority(
completedAuthority,
OutdoorCell));
}
[Fact]
public void BeginReveal_OwnsGenerationDestinationAndSimulationGate()
{