acdream/tests/AcDream.App.Tests/World/RuntimePlacementPresentationSinkTests.cs
Erik e0f96a55bf fix(physics): C4 route 3 — portal placement authority (local player)
Removes a duplicate placement authority for local-player portal arrival.
Portalling worked before this change and works after it — this is not a
bug fix, EXCEPT that it found and fixed one dead-code production bug.

THE PRODUCTION BUG: TryExecuteCanonicalPortalPlacement re-read the
accepted destination at Place time, but TryBeginPortalReveal already
consumes that slot at Aim time — so the arm was 100% dead code and every
real portal Place refused with host-token-unavailable. Found only
because we refused to accept 7 skipped tests instead of chasing the
count to zero.

RETAIL IS THE GENERIC PATH FOR THE THIRD ROUTE RUNNING:
SmartBox::TeleportPlayer @0x00453910 = SetPositionSimple(dest, 1) with
flags 0x1012, followed by PlayerPositionUpdated.

BOTH INVERSIONS, WITH THEIR ANCHORS: unlike route 2, the leash IS armed
here (ConstrainTo @0x0045418A) and velocity is zeroed
(set_velocity @0x004541B4); unlike route 4b-3, the local teleport_hook
runs AFTER placement (@0x004538AE).

THE THREE-ROUND DEFECT CHAIN, HONESTLY:
- Round 1 released the player at the pre-teleport position while the
  anim stream marched on — the contract wrongly assumed Place re-fires
  (process rule 1's third occurrence this campaign).
- Round 2's fix inferred commit from a global PendingCount, which three
  non-committing paths also clear — making the SAME bug complete
  cleanly and silently. Strictly worse than round 1: round 1 at least
  tripped portal-complete-before-materialized.
- Round 3 latches the commit where it actually happens
  (ReconcileAndAcknowledgePortal), keyed on reveal generation and
  teleport sequence, via TryConsumePortalCommit. Two of the three
  required regression tests landed and are sabotage-verified on both
  hosts (ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted /
  HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted).
  The third (force-arm-takes-the-slot) was judged unnecessary on review:
  with the inference gone, PendingCount is only a "don't ask yet" guard
  at both gates, so a force operation occupying or vacating the slot no
  longer changes an input the commit decision reads — the case collapses
  into what the landed test already discriminates.

THE B2/P3 RESOLUTION: both round-2 reviews were right about different
branches of the same synchronous call. RuntimePlacementProjectionSubscription
.OnPlacement acknowledges the FIFO head only when TryApply returns true;
a Place whose portal authority went stale (transit ended/superseded
while parked) used to return false, wedging every later entity's
placement receipt behind it forever. Both sinks
(RuntimePlacementPresentationSink, HeadlessRuntimePlacementProjectionSink)
now acknowledge-and-ignore a stale-authority Place instead of refusing
it. The regression test (RuntimePlacementPresentationSinkTests
.PortalPlace_StaleTransitHostOrSequenceIsAcknowledgedAndIgnored) had
been asserting the old, wrong `false` behaviour; it now asserts and
sabotage-verifies the fix.

Also lands: AP-144 (register discipline — the portal movement-event
send reuses the stricter UsePositionFromServer gate where retail's
SendMovementEvent is the looser autonomy_level != 0 test, diverging
only at level 1, currently unreachable), AP-145 + issue #318 (the
local-player collision-shadow presentation write bypasses its own
publisher's ShadowObjects write via a direct cache .Set(), self-healing
only once dedup diverges — filed, not fixed, pending a composition
test), AD-42 deleted (its last citation retired by the canonical portal
arm), AD-2 updated (the wait-cue's trigger predicate now covers a
second cause), and two documentation corrections: the enter_world
misattribution (both call sites are in SmartBox::HandleCreateObject,
only one in the player branch — portal arrival is TeleportPlayer, not
enter_world) and the stale "local player never reaches this path"
comment on the generic-remote-render-pose write.

Suite: 11,090 passed / 4 skipped / 0 failed. No new skips, nothing
weakened.

STILL OWED: the connected two-client gate, with
ACDREAM_PROBE_LOCAL_TELEPORT=1, scored only if [local-tp] lines
actually appear in the capture — and explicitly NOT scored as covering
issue #318 (no composition test yet asserts PhysicsEngine.ShadowObjects
directly).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 03:57:37 +02:00

1165 lines
46 KiB
C#

using System.Numerics;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.App.Net;
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.Session;
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));
}
/// <summary>
/// The invisible-recalled-remote regression, at the layer that produced
/// it. A restorable park publishes a <c>Withdraw</c> whose PRESENTATION
/// half only this sink performs; <c>RestoreParkWithdrawal</c> restores
/// canonical state and cannot reach any of it, and the per-packet
/// <c>RebucketLiveEntity</c> that used to mask the hole never runs for a
/// remote which parks on its final Position and then stops moving (ACE
/// stops broadcasting for a stationary entity). The entity stayed
/// simulated, collidable, and audible while absent from BOTH the world
/// render and the radar for the rest of the session.
///
/// <para>This test asserts on the presentation side specifically -
/// bucket residency, projection visibility, plugin world state, the
/// world-event replay set, the effect-pose registry, the local-player
/// shadow, and the visibility sinks. Re-checking <c>InWorld</c>, the
/// object clock, and residency (which the shipped Runtime tests already
/// cover) reproduces exactly the blind spot that shipped the bug.</para>
/// </summary>
[Fact]
public void WithdrawalRestored_ReinstatesEveryPresentationRegistrationTheWithdrawalRemoved()
{
Fixture fixture = Fixture.Create();
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
WorldEntity entity = Assert.IsType<WorldEntity>(record.WorldEntity);
AcDream.Plugin.Abstractions.WorldEntitySnapshot expectedSnapshot =
Assert.Single(fixture.WorldState.Entities);
LocalPlayerShadowState.Snapshot? expectedShadow =
fixture.LocalShadow.Current;
Assert.NotNull(expectedShadow);
Assert.Equal(
expectedSnapshot,
Assert.Single(CurrentEventMembership(fixture.WorldEvents)));
Assert.Equal(1, fixture.EffectPoses.Count);
Vector3 posedPosition = entity.Position;
Quaternion posedRotation = entity.Rotation;
RuntimePlacementProjectionSnapshot withdraw = Placement(
fixture,
record,
RuntimePlacementProjectionKind.Withdraw,
posedPosition,
posedRotation);
Assert.True(fixture.Sink.TryApply(in withdraw));
Assert.Empty(fixture.WorldState.Entities);
Assert.Empty(CurrentEventMembership(fixture.WorldEvents));
Assert.Equal(0, fixture.EffectPoses.Count);
RuntimeOwnershipSnapshot beforeRestore =
RuntimeOwnershipSnapshot.Capture(fixture.Runtime, record);
RuntimePlacementProjectionSnapshot restored = Placement(
fixture,
record,
RuntimePlacementProjectionKind.WithdrawalRestored,
// Deliberately NOT the entity's pose: the receipt is the exact
// inverse of a withdrawal, which never moved the sidecar, so the
// restoration must not move it either.
new Vector3(-777f, -777f, -777f),
Quaternion.CreateFromAxisAngle(Vector3.UnitY, 1.25f));
Assert.True(fixture.Sink.TryApply(in restored));
// Graphical projection - what the world render and the radar read.
Assert.True(
record.IsSpatiallyProjected,
"restored park left the entity unprojected");
Assert.True(
record.IsSpatiallyVisible,
"restored park left the entity invisible");
Assert.Contains(record, fixture.Runtime.VisibleRecords);
Assert.True(
fixture.Spatial.IsLiveEntityProjectionResident(
record.ProjectionKey!.Value),
"restored park left the entity out of its draw bucket");
Assert.Equal(SourceCell, entity.ParentCellId);
// Sink-owned registrations - restored EXACTLY, not defaulted.
Assert.Equal(
expectedSnapshot,
Assert.Single(fixture.WorldState.Entities));
Assert.Equal(
expectedSnapshot,
Assert.Single(CurrentEventMembership(fixture.WorldEvents)));
Assert.Equal(1, fixture.EffectPoses.Count);
Assert.True(fixture.EffectPoses.TryGetRootPose(
entity.Id,
out Matrix4x4 restoredPose));
Assert.Equal(
Matrix4x4.CreateFromQuaternion(posedRotation)
* Matrix4x4.CreateTranslation(posedPosition),
restoredPose);
Assert.Equal(expectedShadow, fixture.LocalShadow.Current);
Assert.Equal(
[(record, false), (record, true)],
fixture.Visibility);
// The sidecar pose is untouched by the restoration.
Assert.Equal(posedPosition, entity.Position);
Assert.Equal(posedRotation, entity.Rotation);
// No canonical Runtime ownership moved.
Assert.Equal(
beforeRestore,
RuntimeOwnershipSnapshot.Capture(fixture.Runtime, record));
// Idempotent: re-applying the same immutable receipt (the documented
// IRuntimePlacementProjectionSink contract) changes nothing.
Assert.True(fixture.Sink.TryApply(in restored));
Assert.Equal(
expectedSnapshot,
Assert.Single(fixture.WorldState.Entities));
Assert.Equal(1, fixture.EffectPoses.Count);
Assert.True(record.IsSpatiallyVisible);
}
/// <summary>
/// A WithdrawalRestored receipt must NEVER be refused. Its caller
/// (<c>RuntimePlacementProjectionSubscription</c>) treats a false return
/// as "leave at the FIFO head", which would wedge the ordered stream for
/// every entity - strictly worse than the invisibility the receipt
/// repairs. Every way the restore can decline is an entity that is gone,
/// displaced, or unmaterialized, i.e. one with no prior projection left.
/// </summary>
[Fact]
public void WithdrawalRestored_IsAcknowledgedEvenWhenTheProjectionIsGone()
{
Fixture fixture = Fixture.Create();
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
RuntimePlacementProjectionSnapshot restored = Placement(
fixture,
record,
RuntimePlacementProjectionKind.WithdrawalRestored,
Vector3.Zero,
Quaternion.Identity) with
{
Token = Placement(
fixture,
record,
RuntimePlacementProjectionKind.WithdrawalRestored,
Vector3.Zero,
Quaternion.Identity).Token with
{
SessionLifetimeVersion =
fixture.Runtime.SessionLifetimeVersion + 99UL,
},
};
Assert.True(fixture.Sink.TryApply(in restored));
Assert.Empty(fixture.Visibility);
}
private static List<AcDream.Plugin.Abstractions.WorldEntitySnapshot>
CurrentEventMembership(WorldEvents events)
{
var replayed = new List<AcDream.Plugin.Abstractions.WorldEntitySnapshot>();
void Handler(AcDream.Plugin.Abstractions.WorldEntitySnapshot snapshot) =>
replayed.Add(snapshot);
events.EntitySpawned += Handler;
events.EntitySpawned -= Handler;
return replayed;
}
[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 ExecutorCompleted_IsAckOnlyNoOpEvenWhenTokenIsStaleOrSidecarIsGone()
{
// F1: mirrors Discard_IsAckOnlyNoOpEvenWhenTokenIsStaleOrSidecarIsGone
// exactly - proves ExecutorCompleted is acknowledged unconditionally
// (never wedges the FIFO on a record-lookup failure) and never
// mutates presentation state, even under a completely bogus token.
Fixture fixture = Fixture.Create();
LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell));
WorldEntity entity = record.WorldEntity!;
RuntimePlacementProjectionSnapshot completion = Placement(
fixture,
record,
RuntimePlacementProjectionKind.ExecutorCompleted,
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 completion));
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);
}
/// <summary>
/// B2 review fix (2026-08-05): a Place whose portal authority is stale
/// (the transit ended or was superseded while a park sat outstanding)
/// must be ACKNOWLEDGED-AND-IGNORED, not refused. Before the fix this
/// test asserted <c>TryApply</c> returns <c>false</c> for both the
/// superseded-host and wrong-sequence cases — but
/// <c>RuntimePlacementProjectionSubscription.OnPlacement</c> only calls
/// <c>Acknowledge</c> when <c>TryApply</c> returns <c>true</c>; a
/// <c>false</c> here leaves the stale receipt at the FIFO head
/// PERMANENTLY, wedging every later entity's placement receipt behind
/// it. The entity's world position must still never move to the stale
/// receipt's coordinates — "ignore" means the receipt is retired
/// without being applied, not silently accepted as real.
/// </summary>
[Fact]
public void PortalPlace_StaleTransitHostOrSequenceIsAcknowledgedAndIgnored()
{
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),
};
// Acknowledged (true) - the stale-authority Place is retired from
// the FIFO head, not left wedged - but the position never moves to
// the stale receipt's coordinates.
Assert.True(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.True(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);
}
[Fact]
public void GraphicalRoute_ReentrantInitialDrainTeardownLeavesHeadForReplacement()
{
using var fixture = SubscriptionFixture.Create();
Assert.True(fixture.Lifetime.Physics.SetPosition.Cancel(
fixture.Record.Canonical,
publishWithdrawal: true));
Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
RuntimeGenerationToken generation = fixture.Generation;
var retries = new RuntimePlacementProjectionRetrySlot(
() => generation);
var firstEvents = new RecordingEventRoute();
GraphicalSessionEventRoute? first = null;
int applications = 0;
var sink = new DelegatePlacementSink(
(in RuntimePlacementProjectionSnapshot projection) =>
{
applications++;
bool applied = fixture.Sink.TryApply(in projection);
if (applications == 1)
first!.Dispose();
return applied;
});
first = new GraphicalSessionEventRoute(
firstEvents,
() => new RuntimePlacementProjectionSubscription(
fixture.Lifetime.Placements,
() => generation,
sink,
retryPendingOnSubscribe: false),
() => generation,
retries);
first.Attach();
Assert.Equal(1, applications);
Assert.Equal(1, fixture.Lifetime.Placements.PendingCount);
Assert.Equal(0, retries.BindingCount);
Assert.Equal(0, fixture.Lifetime.Events.PlacementSubscriberCount);
Assert.Equal(1, firstEvents.DisposeCount);
var replacementEvents = new RecordingEventRoute();
var replacement = new GraphicalSessionEventRoute(
replacementEvents,
() => new RuntimePlacementProjectionSubscription(
fixture.Lifetime.Placements,
() => generation,
sink,
retryPendingOnSubscribe: false),
() => generation,
retries);
replacement.Attach();
Assert.Equal(2, applications);
Assert.Equal(0, fixture.Lifetime.Placements.PendingCount);
Assert.Equal(1, retries.BindingCount);
Assert.Equal(1, fixture.Lifetime.Events.PlacementSubscriberCount);
replacement.Dispose();
Assert.Equal(0, retries.BindingCount);
Assert.Equal(0, fixture.Lifetime.Events.PlacementSubscriberCount);
Assert.Equal(1, replacementEvents.DisposeCount);
}
[Fact]
public void RetrySlot_InvokesOnlyExactCurrentGenerationBinding()
{
RuntimeGenerationToken generation = new(7UL);
var retries = new RuntimePlacementProjectionRetrySlot(
() => generation);
int firstCalls = 0;
IDisposable first = retries.BindOwned(
generation,
() =>
{
firstCalls++;
return true;
});
retries.RetryPending();
generation = new RuntimeGenerationToken(8UL);
retries.RetryPending();
first.Dispose();
int replacementCalls = 0;
using IDisposable replacement = retries.BindOwned(
generation,
() =>
{
replacementCalls++;
return true;
});
first.Dispose();
retries.RetryPending();
Assert.Equal(1, firstCalls);
Assert.Equal(1, replacementCalls);
Assert.Equal(1, retries.BindingCount);
}
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);
// C3c fixture normalization: RegisterAndMaterializeProjection's
// legacy-immediate rebucket commits the wire cell out-of-band of
// the fresh initial-create residence, leaving that residence
// stale. Converge it deterministically HERE (the query performs
// the lazy retirement, releasing the residence's never-driven
// initial SetPosition operation) so ownership snapshots captured
// by tests reflect the settled post-registration state instead of
// shifting inside the sink's own first residence-gate query.
Assert.False(Runtime.HasActiveInitialCreateResidence(
record.Canonical));
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 DelegatePlacementSink(
PlacementApply apply) : IRuntimePlacementProjectionSink
{
public bool TryApply(
in RuntimePlacementProjectionSnapshot projection) =>
apply(in projection);
}
private delegate bool PlacementApply(
in RuntimePlacementProjectionSnapshot projection);
private sealed class RecordingEventRoute : ILiveSessionEventRouting
{
public int AttachCount { get; private set; }
public int DisposeCount { get; private set; }
public void Attach() => AttachCount++;
public void Dispose() => DisposeCount++;
}
private sealed class RecordingResources : ILiveEntityResourceLifecycle
{
public void Register(WorldEntity entity) { }
public void Unregister(WorldEntity entity) { }
}
}