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>
1165 lines
46 KiB
C#
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) { }
|
|
}
|
|
}
|