acdream/tests/AcDream.Runtime.Tests/Entities/RuntimeRemoteFirstEntryStateTests.cs
Erik 6dcb94ac1b test(runtime): restore the world-frame precondition across first-entry fixtures
670f307c made remote first-entry placement resolve its landblock-local
CreateObject origin through Runtime's world frame
(RuntimeSetPositionState.PrepareMover:1526-1544) and return
RetrySetupUnavailable until that frame exists. Only the accepted local-player
Create publishes it (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570 ->
RuntimePhysicsState.ObserveLocalWorldFrame).

Fixtures that drive remote conductors in a world with no local player - a
state production never occupies, since the player's own Create always precedes
broadcast Creates - therefore parked forever on RetrySetupUnavailable. Their
initial-create residences never retired, which cascaded into rejected
appearance updates, missing canonical bodies, unconverged ownership ledgers,
and a GameRuntime teardown that could not complete stage 10.

The measured blast radius was far larger than the handoff recorded. It claimed
"six selected fixture failures"; a baseline run found 43. The App suite was
fully green at 01f4791e and 670f307c broke 28 tests at once; the Runtime suite
lost 13, twelve of them in RuntimeRemoteFirstEntryStateTests - the exact
conductor that commit gated. Both commits were verified on focused runs only.

The production gate is correct, so nothing here weakens it. It matches App's
own coordinate owner: LiveWorldOriginState is initialized once from the local
player's spawn (LiveEntityHydrationPorts.cs:226) and rebased only by
StreamingOriginRecenterCoordinator.Advance at a teleport boundary - exactly
ObserveLocalWorldFrame's semantics. Every fixture is repaired by supplying the
missing precondition beside the resident landblock it already models, and not
one expected value or assertion was changed.

The mechanism shipped with zero tests. RuntimeWorldFrameTests now pins its
contract: the local player publishes the frame, remotes never do, neighbouring
landblocks convert at 192 m per step, ordinary movement across a landblock
boundary must NOT rebase it, an accepted teleport must, and a zero cell id
neither publishes nor resolves. That "no rebase on ordinary movement" rule is
load-bearing - if it and LiveWorldOriginState ever disagree, remote objects
are placed a multiple of 192 m from where the world is streamed.

Runtime 1,009/1,009; App 4,048 passed / 3 skipped.

Refs #281.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 13:32:28 +02:00

995 lines
42 KiB
C#

using System.Collections.Immutable;
using System.Numerics;
using AcDream.Content;
using AcDream.Content.Pak;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
using AcDream.Runtime;
namespace AcDream.Runtime.Tests.Entities;
public sealed class RuntimeRemoteFirstEntryStateTests
{
private const uint Landblock = 0xA9B60000u;
private const uint Cell = Landblock | 0x0001u;
private const uint SetupId = 0x02000001u;
private static readonly RuntimeInitialCreateExecutionInputs NoContact =
new(UsePositionFromServer: false, PlayerDistance: 0f);
// ---------------------------------------------------------------
// Happy path
// ---------------------------------------------------------------
[Fact]
public void FullSequenceRemoteEntryConstructsGatedBodyPlacesAndCompletes()
{
using var fixture = new Fixture(residentWorld: true);
RuntimeRemoteFirstEntryStatus status = fixture.Advance(
out RuntimeInitialCreateExecutionReceipt receipt,
out RuntimeRemoteBodyConstructionReceipt construction);
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed, status);
Assert.Equal(Cell, receipt.FullCellId);
// Remote Creates carry NO teleport hook (classifier: AfterEnterWorld
// is local-player-only).
Assert.Equal(RuntimeTeleportHookPhase.None, receipt.TeleportHookPhase);
// M1: the terminal Advance delivers the construction receipt in the
// same call that reaps its retained storage — the C3c host's one
// natural consumption point.
Assert.True(construction.MotionTableGatePassed);
Assert.Equal(0x09000001u, construction.MotionTableId);
Assert.False(construction.MovementBranch);
Assert.True(construction.PlacementFrameStaged);
Assert.True(construction.FrictionApplied);
Assert.Equal(0.5f, construction.Friction);
Assert.False(construction.TranslucencyApplied);
Assert.Equal(0f, construction.TranslucencyOriginal);
Assert.True(construction.VelocityApplied);
Assert.True(construction.OmegaApplied);
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
Assert.True(body.InWorld);
Assert.Equal(Cell, fixture.Record.FullCellId);
// set_description field application from the frozen wire desc:
// friction 0.5 passed gates 1+2; elasticity 0.05 inside the
// set_elasticity clamp; state is the record's retail-transitioned
// view; omega was the raw step-11 write.
Assert.Equal(0.5f, body.Friction);
Assert.Equal(0.05f, body.Elasticity);
Assert.Equal(fixture.Record.FinalPhysicsState, body.State);
Assert.Equal(new Vector3(0f, 0f, 0.25f), body.Omega);
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
Assert.False(fixture.Conductor.TryGetConstruction(
fixture.Key, out _));
Assert.False(fixture.Lifetime.TryGetInitialCreateResidence(
fixture.Record, out _));
Assert.Equal(0, fixture.Lifetime.CaptureOwnership()
.InitialCreateResidenceLeaseCount);
Assert.Equal(0, fixture.Lifetime.CaptureOwnership()
.InitialCreateExecutorProgressCount);
Assert.Equal(0, fixture.Lifetime.CaptureOwnership()
.RemoteFirstEntryActiveCount);
// Retrying with the now-stale residence token is a distinct, safe
// no-op — nothing left to resume.
Assert.Equal(
RuntimeRemoteFirstEntryStatus.RejectedToken,
fixture.Advance(out _));
}
[Fact]
public void ProjectileFlavorFullSequenceCompletesWithMissileState()
{
using var fixture = new Fixture(
residentWorld: true,
rawState: (uint)(PhysicsStateFlags.Gravity
| PhysicsStateFlags.ReportCollisions
| PhysicsStateFlags.Missile
| PhysicsStateFlags.Inelastic
| PhysicsStateFlags.PathClipped
| PhysicsStateFlags.AlignPath));
// The Missile flag classified the Create to ProjectileAuthoritative
// (RuntimeInitialCreateResidenceState.Begin) — the conductor accepts
// that flavor through the identical stages.
Assert.True(fixture.Lifetime.TryGetInitialCreateResidence(
fixture.Record, out RuntimeInitialCreateResidenceLease lease));
Assert.Equal(
RuntimeSetPositionOperationKind.ProjectileAuthoritative,
lease.Route.OperationKind);
RuntimeRemoteFirstEntryStatus status = fixture.Advance(
out RuntimeInitialCreateExecutionReceipt receipt);
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed, status);
Assert.Equal(Cell, receipt.FullCellId);
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
Assert.True(body.InWorld);
Assert.True(body.State.HasFlag(PhysicsStateFlags.Missile));
Assert.Equal(fixture.Record.FinalPhysicsState, body.State);
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
}
// ---------------------------------------------------------------
// The three float gates, proven both ways on the pure construction
// (docs/research/2026-08-02-set-description-float-gates.md is law)
// ---------------------------------------------------------------
[Theory]
[InlineData(-1f, false, 0.95f)]
[InlineData(0f, true, 0f)]
[InlineData(0.5f, true, 0.5f)]
[InlineData(1f, true, 1f)]
[InlineData(1.5f, false, 0.95f)]
// R2b: NaN is SKIPPED (body keeps its default) — the gates doc's
// sanctioned deviation from retail's unordered-goes-to-apply codegen
// quirk; see the friction comment in RuntimeRemoteBodyDescription.
[InlineData(float.NaN, false, 0.95f)]
public void FrictionGateAppliesOnlyInsideClosedUnitInterval(
float wireFriction,
bool expectedApplied,
float expectedBodyFriction)
{
(PhysicsBody body, RuntimeRemoteBodyConstructionReceipt receipt) =
ConstructDirect(friction: wireFriction);
Assert.Equal(expectedApplied, receipt.FrictionApplied);
Assert.Equal(expectedBodyFriction, body.Friction);
Assert.Equal(body.Friction, receipt.Friction);
}
[Fact]
public void FrictionAbsentOnWireTakesTheDescConstructorDefault()
{
// PhysicsDesc ctor 0x0051D4D0 seeds friction 0.95f; it passes the
// gate, so an absent wire friction still records an APPLIED write.
(PhysicsBody body, RuntimeRemoteBodyConstructionReceipt receipt) =
ConstructDirect(friction: null);
Assert.True(receipt.FrictionApplied);
Assert.Equal(0.95f, body.Friction);
}
[Theory]
[InlineData(0f, false)]
[InlineData(0.5f, true)]
[InlineData(-0.25f, true)]
// R2: NaN lands in the apply bucket — matching retail's unordered case
// exactly (gates doc conditional 3: JNP fires ONLY on exact equality
// with 0.0f).
[InlineData(float.NaN, true)]
public void TranslucencyGateAppliesOnlyWhenNonZeroAndOriginalIsAlwaysRecorded(
float wireTranslucency,
bool expectedApplied)
{
(_, RuntimeRemoteBodyConstructionReceipt receipt) =
ConstructDirect(translucency: wireTranslucency);
// translucencyOriginal is written UNCONDITIONALLY (0x00515097,
// before the gate); the live apply fires for every value except
// exact 0.0f (gate 3's JNP fires only on equality).
Assert.Equal(wireTranslucency, receipt.TranslucencyOriginal);
Assert.Equal(expectedApplied, receipt.TranslucencyApplied);
}
[Fact]
public void TranslucencyAbsentOnWireIsZeroOriginalAndNoLiveApply()
{
(_, RuntimeRemoteBodyConstructionReceipt receipt) =
ConstructDirect(translucency: null);
Assert.Equal(0f, receipt.TranslucencyOriginal);
Assert.False(receipt.TranslucencyApplied);
}
[Theory]
[InlineData(-0.5f, 0f)]
[InlineData(0f, 0f)]
[InlineData(0.05f, 0.05f)]
[InlineData(0.1f, 0.1f)]
[InlineData(0.2f, 0.1f)]
// R2a: retail's FIRST x87 compare sends unordered to the zeroing arm
// (0x0050FD51) — NaN -> 0f, NOT ACE's divergent NaN -> 0.1f.
[InlineData(float.NaN, 0f)]
public void ElasticityClampMatchesRetailSetter(
float wireElasticity,
float expectedBodyElasticity)
{
// CPhysicsObj::set_elasticity 0x0050FD40: < 0 -> 0; <= 0.1 -> value;
// > 0.1 -> 0.1 (ACE PhysicsGlobals.MaxElasticity = 0.1f agrees).
(PhysicsBody body, RuntimeRemoteBodyConstructionReceipt receipt) =
ConstructDirect(elasticity: wireElasticity);
Assert.Equal(expectedBodyElasticity, body.Elasticity);
Assert.Equal(body.Elasticity, receipt.Elasticity);
}
// ---------------------------------------------------------------
// Placement-frame-vs-movement branch (set_description step 4)
// ---------------------------------------------------------------
[Fact]
public void MovementPayloadSuppressesPlacementFrameAndWritesAutonomy()
{
(PhysicsBody body, RuntimeRemoteBodyConstructionReceipt receipt) =
ConstructDirect(movement: new PhysicsMovementData(
RawData: new byte[] { 0x01 },
MotionState: null,
IsAutonomous: true));
Assert.True(receipt.MovementBranch);
Assert.True(receipt.LastMoveWasAutonomous);
Assert.True(body.LastMoveWasAutonomous);
Assert.False(receipt.PlacementFrameStaged);
// No frame staged: the body's cell identity stays default-detached.
Assert.Equal(0u, body.CellPosition.ObjCellId);
Assert.False(body.InWorld);
}
[Fact]
public void MovementFlagWithEmptyBufferTakesThePlacementBranch()
{
// R1: retail's branch discriminator is `movement_buffer != 0` —
// PhysicsDesc::UnPack (0x0051DDD0) assigns the buffer only inside
// `if (buff_length != 0)` (0051DE1F-0051DE2A), so a movement flag
// with a ZERO-LENGTH buffer reaches set_description with a null
// buffer and takes the PLACEMENT branch; the autonomy write (in the
// movement else-branch only) never runs. Our parser materializes
// exactly this wrapper shape (CreateObject.cs: non-null
// PhysicsMovementData with empty RawData). IsAutonomous is set true
// here deliberately — the BRANCH, never the wrapper's value, must
// decide.
(PhysicsBody body, RuntimeRemoteBodyConstructionReceipt receipt) =
ConstructDirect(movement: new PhysicsMovementData(
RawData: ReadOnlyMemory<byte>.Empty,
MotionState: null,
IsAutonomous: true));
Assert.False(receipt.MovementBranch);
Assert.True(receipt.PlacementFrameStaged);
Assert.Equal(Cell, body.CellPosition.ObjCellId);
Assert.False(body.InWorld);
Assert.False(receipt.LastMoveWasAutonomous);
Assert.False(body.LastMoveWasAutonomous);
}
[Fact]
public void NoMovementPayloadStagesTheDormantPlacementFrame()
{
(PhysicsBody body, RuntimeRemoteBodyConstructionReceipt receipt) =
ConstructDirect(movement: null);
Assert.False(receipt.MovementBranch);
Assert.True(receipt.PlacementFrameStaged);
Assert.Equal(Cell, body.CellPosition.ObjCellId);
// SetPlacementFrameInternal is NOT enter_world — the submission owns
// world residence.
Assert.False(body.InWorld);
Assert.False(body.LastMoveWasAutonomous);
}
// ---------------------------------------------------------------
// Yield flavors + retry idempotency per stage
// ---------------------------------------------------------------
[Fact]
public void AwaitingCollisionSourceRetriesThenResumesOnceSetupLands()
{
using var fixture = new Fixture(residentWorld: true, setupTableId: SetupId);
fixture.CollisionSource.Status = PreparedAssetReadStatus.Missing;
RuntimeRemoteFirstEntryStatus first = fixture.Advance(out _);
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingCollisionSource, first);
// No progress entry, no body, nothing mutated while the Setup is
// outstanding.
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
Assert.Null(fixture.Record.PhysicsBody);
RuntimeRemoteFirstEntryStatus second = fixture.Advance(out _);
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingCollisionSource, second);
Assert.True(fixture.CollisionSource.ReadCount >= 2);
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
Assert.Null(fixture.Record.PhysicsBody);
fixture.CollisionSource.Status = PreparedAssetReadStatus.Loaded;
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed,
fixture.Advance(out RuntimeInitialCreateExecutionReceipt receipt));
Assert.Equal(Cell, receipt.FullCellId);
Assert.NotNull(fixture.Record.PhysicsBody);
}
[Fact]
public void DeferredPlacementRetainsOneBodyIdentityAcrossRetriesThenWakesAndCompletes()
{
// Non-resident world: the ordinary submission defers (the
// destination landblock's collision world was never added), parking
// the operation behind a Withdraw the conductor drains itself.
using var fixture = new Fixture(residentWorld: false);
RuntimeRemoteFirstEntryStatus first = fixture.Advance(out _);
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingPlacement, first);
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
Assert.False(body.InWorld);
Assert.Equal(0u, fixture.Record.FullCellId);
// The construction is still inspectable mid-flight, and the
// set_description writes are already on the body.
Assert.True(fixture.Conductor.TryGetConstruction(
fixture.Key, out RuntimeRemoteBodyConstructionReceipt construction));
Assert.True(construction.FrictionApplied);
Assert.Equal(0.5f, body.Friction);
Assert.True(construction.VelocityApplied);
Assert.Equal(new Vector3(1f, 2f, 0.5f), body.Velocity);
Assert.True(construction.OmegaApplied);
Assert.Equal(1, fixture.Conductor.CaptureOwnership().ActiveCount);
// Retry idempotency: the SAME body identity survives every retry —
// no duplicate construction, no re-submission churn.
for (int i = 0; i < 3; i++)
{
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingPlacement,
fixture.Advance(out _));
Assert.Same(body, fixture.Record.PhysicsBody);
Assert.Equal(1, fixture.Conductor.CaptureOwnership().ActiveCount);
}
const ulong generation = 1UL;
fixture.Lifetime.Physics.SetPosition.BeginCollisionGeneration(
Landblock, generation);
fixture.Lifetime.Physics.Engine.AddLandblock(
Landblock,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
Landblock, generation, ready: true);
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed,
fixture.Advance(out RuntimeInitialCreateExecutionReceipt receipt));
Assert.Same(body, fixture.Record.PhysicsBody);
Assert.True(body.InWorld);
Assert.Equal(Cell, receipt.FullCellId);
Assert.Equal(Cell, fixture.Record.FullCellId);
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
}
[Fact]
public void ReentrantAdvanceDuringCollisionCallbackFailsClosedWithContentionAndOuterCallStillCompletes()
{
using var fixture = new Fixture(residentWorld: true);
bool reentered = false;
RuntimeRemoteFirstEntryStatus? innerStatus = null;
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
(_, phase, _, observed) =>
{
if (!reentered && phase is TransitionCellCollisionPhase.Environment)
{
reentered = true;
innerStatus = fixture.Advance(out _);
}
return observed;
};
RuntimeRemoteFirstEntryStatus outerStatus = fixture.Advance(
out RuntimeInitialCreateExecutionReceipt receipt);
Assert.True(reentered);
Assert.Equal(RuntimeRemoteFirstEntryStatus.Contention, innerStatus);
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed, outerStatus);
Assert.Equal(Cell, receipt.FullCellId);
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
}
[Fact]
public void AwaitingReceiptAcknowledgementWhileAnotherEntityHoldsTheFifoHeadThenResumes()
{
using var fixture = new Fixture(residentWorld: true);
(RuntimeEntityRecord other, RuntimePlacementProjectionToken otherToken) =
BeginPendingOrdinaryPlacement(fixture, 0x70099001u);
// Our submission commits, but its Place sits BEHIND the other
// entity's unacknowledged receipt in the one ordered FIFO.
RuntimeRemoteFirstEntryStatus first = fixture.Advance(out _);
Assert.Equal(
RuntimeRemoteFirstEntryStatus.AwaitingReceiptAcknowledgement,
first);
Assert.True(fixture.Record.PhysicsBody!.InWorld);
RuntimeRemoteFirstEntryStatus second = fixture.Advance(out _);
Assert.Equal(
RuntimeRemoteFirstEntryStatus.AwaitingReceiptAcknowledgement,
second);
Assert.True(fixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(otherToken));
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed,
fixture.Advance(out RuntimeInitialCreateExecutionReceipt receipt));
Assert.Equal(Cell, receipt.FullCellId);
_ = other;
}
[Fact]
public void DeleteWhileAwaitingReceiptAcknowledgementAbandonsInsteadOfRetryingForeverAndConverges()
{
// M2a — the exact C3a-shaped scenario: the placement has ALREADY
// committed (body in world) but acknowledgement is blocked behind
// another entity's unacknowledged Place. The delete-path mechanism
// (Physics.SetPosition.Forget with releasePreparedMover — the exact
// inner call TryAcceptDelete makes) rewrites our still-pending Place
// slot to Discard with a bumped revision, so the cached
// progress.Projection can never match the FIFO head again. Without
// the shared IsAcknowledgementStillPending re-check, Advance would
// report AwaitingReceiptAcknowledgement forever and ownership would
// never converge.
using var fixture = new Fixture(residentWorld: true);
(RuntimeEntityRecord other, RuntimePlacementProjectionToken otherToken) =
BeginPendingOrdinaryPlacement(fixture, 0x70099002u);
Assert.Equal(
RuntimeRemoteFirstEntryStatus.AwaitingReceiptAcknowledgement,
fixture.Advance(out _));
Assert.Equal(1, fixture.Conductor.CaptureOwnership().ActiveCount);
Assert.True(fixture.Record.PhysicsBody!.InWorld);
Assert.True(fixture.Lifetime.TryGetInitialCreateResidence(
fixture.Record, out _));
RuntimePlacementCancellationReceipt cancellation = fixture.Lifetime
.Physics.SetPosition.Forget(
fixture.Record,
releasePreparedMover: true);
fixture.Lifetime.Physics.SetPosition.PublishCancellation(cancellation);
Assert.Equal(RuntimeRemoteFirstEntryStatus.RejectedAuthority,
fixture.Advance(out _));
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
// The unrelated entity's own placement is untouched and still
// acknowledgeable — the abandonment never reached past our own
// entity's projection.
Assert.True(fixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(otherToken));
_ = other;
}
[Fact]
public void AwaitingContinuationPlacementPropagatesExecutorYieldThenResumes()
{
using var fixture = new Fixture(residentWorld: true);
// A fresh Position arrives while the initial residence is still
// pending — enqueued as a continuation, classified only at drain.
WorldSession.EntityPositionUpdate update = new(
fixture.Record.ServerGuid,
new CreateObject.ServerPosition(Cell, 40f, 20f, 7f, 1f, 0f, 0f, 0f),
Velocity: null,
PlacementId: 2,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 1,
ForcePositionSequence: 0);
Assert.True(fixture.Lifetime.TryApplyPosition(
update,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
projectionRequiresTeleportHook: false,
acknowledgeProjection: null,
out PositionTimestampDisposition disposition,
out _,
out _));
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
RuntimeRemoteFirstEntryStatus status = fixture.Advance(out _);
Assert.Equal(
RuntimeRemoteFirstEntryStatus.AwaitingContinuationPlacement,
status);
// The conductor's own sequence reached its terminal Acknowledged
// stage; the retained progress entry lets a retry skip straight to
// re-calling Execute.
Assert.Equal(1, fixture.Conductor.CaptureOwnership().ActiveCount);
RuntimeEntityKey key = fixture.Key;
Assert.True(fixture.Lifetime.InitialCreateExecution
.TryGetPendingContinuationPlacement(
key, out RuntimeEntityPlacementToken placement));
Assert.True(fixture.Lifetime.InitialCreateExecution
.TryGetPendingContinuationRoute(
key, out RuntimeAuthoritativePositionRoute route));
CompleteOrdinaryPlacement(fixture, placement, route);
RuntimeRemoteFirstEntryStatus resumed = fixture.Advance(
out RuntimeInitialCreateExecutionReceipt receipt);
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed, resumed);
Assert.Contains(receipt.Trace,
a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
}
// ---------------------------------------------------------------
// Domain refusal + never-clobber
// ---------------------------------------------------------------
[Fact]
public void LocalPlayerLeaseIsRefusedWithoutTrackingAnything()
{
using var fixture = new Fixture(residentWorld: true, isLocalPlayer: true);
Assert.Equal(RuntimeRemoteFirstEntryStatus.RejectedToken,
fixture.Advance(out _));
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
Assert.Null(fixture.Record.PhysicsBody);
// The lease itself is untouched — the C3a conductor still owns it.
Assert.True(fixture.Lifetime.TryGetInitialCreateResidence(
fixture.Record, out RuntimeInitialCreateResidenceLease lease));
Assert.Equal(fixture.Lease.Token, lease.Token);
}
[Fact]
public void ForeignBodyBoundOutOfBandAbandonsWithoutClobbering()
{
using var fixture = new Fixture(residentWorld: true);
var foreign = new PhysicsBody
{
Position = new Vector3(1f, 2f, 3f),
State = fixture.Record.FinalPhysicsState,
};
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, foreign);
Assert.Equal(RuntimeRemoteFirstEntryStatus.RejectedAuthority,
fixture.Advance(out _));
// The foreign body was never replaced or mutated toward ours.
Assert.Same(foreign, fixture.Record.PhysicsBody);
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
}
// ---------------------------------------------------------------
// Delete / reset mid-flight convergence
// ---------------------------------------------------------------
[Fact]
public void DeleteWhileAwaitingPlacementConvergesAutomaticallyThroughTheRetirementFanOut()
{
using var fixture = new Fixture(residentWorld: false);
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingPlacement,
fixture.Advance(out _));
Assert.Equal(1, fixture.Conductor.CaptureOwnership().ActiveCount);
DeleteEntity(fixture);
Assert.Null(fixture.Record.Key);
// TryAcceptDelete's ForgetInitialCreateResidence fired the multicast
// retirement notification synchronously — this conductor's Forget is
// bound into that same fan-out, so convergence is complete before
// delete returns.
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
// Retrying Advance afterward is a safe, distinct no-op.
Assert.Equal(RuntimeRemoteFirstEntryStatus.RejectedToken,
fixture.Advance(out _));
}
[Fact]
public void SessionClearMidFlightConvergesOwnership()
{
using var fixture = new Fixture(residentWorld: false);
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingPlacement,
fixture.Advance(out _));
Assert.Equal(1, fixture.Conductor.CaptureOwnership().ActiveCount);
_ = fixture.Lifetime.BeginSessionClear();
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
Assert.Equal(0, fixture.Lifetime.CaptureOwnership()
.RemoteFirstEntryActiveCount);
}
[Fact]
public void DeleteAndSameGuidReincarnationStartsAFreshSequence()
{
using var fixture = new Fixture(residentWorld: false);
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingPlacement,
fixture.Advance(out _));
RuntimeEntityKey staleKey = fixture.Key;
uint guid = fixture.Record.ServerGuid;
DeleteEntity(fixture);
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
RuntimeEntityRecord reincarnated = fixture.Lifetime
.RegisterEntityWithInitialResidence(
Spawn(guid, incarnation: 2),
isLocalPlayer: false)
.Canonical!;
Assert.NotEqual(staleKey, reincarnated.Key!.Value);
Assert.True(fixture.Lifetime.TryGetInitialCreateResidence(
reincarnated,
out RuntimeInitialCreateResidenceLease freshLease));
fixture.Record = reincarnated;
fixture.Lease = freshLease;
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingPlacement,
fixture.Advance(out _));
const ulong generation = 1UL;
fixture.Lifetime.Physics.SetPosition.BeginCollisionGeneration(
Landblock, generation);
fixture.Lifetime.Physics.Engine.AddLandblock(
Landblock,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
Landblock, generation, ready: true);
RuntimeRemoteFirstEntryStatus status = fixture.Advance(
out RuntimeInitialCreateExecutionReceipt receipt);
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed, status);
Assert.Equal(Cell, receipt.FullCellId);
Assert.NotNull(reincarnated.PhysicsBody);
Assert.True(reincarnated.PhysicsBody!.InWorld);
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
}
// ---------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------
private static (PhysicsBody Body, RuntimeRemoteBodyConstructionReceipt Receipt)
ConstructDirect(
float? friction = 0.5f,
float? elasticity = 0.05f,
float? translucency = null,
PhysicsMovementData? movement = null)
{
var record = new RuntimeEntityRecordFactory();
RuntimeEntityRecord canonical = record.Lifetime
.RegisterEntity(
Spawn(
0x70090077u,
incarnation: 1,
friction: friction,
elasticity: elasticity,
translucency: translucency,
movement: movement))
.Canonical!;
var command = new RuntimeSetPositionCommand(
new PhysicsSetPositionRequest(
new Vector3(1f, 2f, 3f),
Quaternion.Identity,
Cell,
new Vector3(1f, 2f, 3f),
ImmutableArray<FlatCollisionSphere>.Empty,
Scale: 1f,
StepUpHeight: 0f,
StepDownHeight: 0f,
canonical.FinalPhysicsState,
ObjectInfoState.None,
canonical.Key?.LocalEntityId ?? 0u,
PhysicsPlacementClass.Ordinary,
PhysicsSetPositionFlags.Placement | PhysicsSetPositionFlags.Slide),
RuntimeSetPositionOperationKind.RemoteAuthoritative,
GameTime: 1d,
ExpectedVelocityAuthorityVersion: 0UL);
PhysicsBody body = RuntimeRemoteBodyDescription.Construct(
canonical,
canonical.Snapshot.Physics,
command,
out RuntimeRemoteBodyConstructionReceipt receipt);
record.Dispose();
return (body, receipt);
}
private sealed class RuntimeEntityRecordFactory : IDisposable
{
internal RuntimeEntityObjectLifetime Lifetime { get; } = new();
internal RuntimeEntityRecordFactory()
{
var generation = new RuntimeGenerationToken(1UL);
Lifetime.BindEventContext(() => generation, static () => 1UL);
}
public void Dispose() => Lifetime.Dispose();
}
private static (RuntimeEntityRecord Record, RuntimePlacementProjectionToken Token)
BeginPendingOrdinaryPlacement(Fixture fixture, uint guid)
{
RuntimeEntityRecord record = fixture.Lifetime.RegisterEntity(
Spawn(guid, incarnation: 1)).Canonical!;
var body = new PhysicsBody
{
Position = new Vector3(50f, 50f, 3f),
Orientation = Quaternion.Identity,
State = record.FinalPhysicsState,
};
body.SnapToCell(Cell, body.Position, body.Position);
fixture.Lifetime.Entities.SetPhysicsBody(record, body);
RuntimeEntityPlacementToken placement = fixture.Lifetime.Physics
.SetPosition.BeginAuthoredPlacement(
record,
record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.RemoteAuthoritative);
Assert.True(placement.IsValid);
var preparation = new RuntimeSetPositionMoverPreparation(
RuntimeSetPositionMoverSetup.ResolvedAbsent,
RuntimeSetPositionOperationKind.RemoteAuthoritative,
GameTime: 1d,
PhysicsPlacementClass.Ordinary,
PhysicsSetPositionFlags.Placement | PhysicsSetPositionFlags.Slide);
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
fixture.Lifetime.Physics.SetPosition.PrepareMover(
placement, preparation, out RuntimeSetPositionCommand command));
RuntimeSetPositionOutcome outcome = fixture.Lifetime.Physics.SetPosition
.SubmitPreparedPlacement(placement, command);
Assert.Equal(RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
outcome.Status);
return (record, outcome.Projection);
}
private static void CompleteOrdinaryPlacement(
Fixture fixture,
in RuntimeEntityPlacementToken placement,
in RuntimeAuthoritativePositionRoute route)
{
var preparation = new RuntimeSetPositionMoverPreparation(
RuntimeSetPositionMoverSetup.ResolvedAbsent,
route.OperationKind,
GameTime: 1d,
PhysicsPlacementClass.Ordinary,
route.SetPositionFlags);
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
fixture.Lifetime.Physics.SetPosition.PrepareMover(
placement, preparation, out RuntimeSetPositionCommand command));
RuntimeSetPositionOutcome outcome = fixture.Lifetime.Physics.SetPosition
.SubmitPreparedPlacement(placement, command);
Assert.Equal(RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
outcome.Status);
Assert.True(fixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(outcome.Projection));
}
private static void DeleteEntity(Fixture fixture)
{
Assert.True(fixture.Lifetime.TryAcceptDelete(
new DeleteObject.Parsed(
fixture.Record.ServerGuid, fixture.Record.Incarnation),
isLocalPlayer: false,
removeRetainedObject: false,
out RuntimeEntityDeleteAcceptance acceptance));
fixture.Lifetime.CompleteAcceptedDelete(acceptance);
Assert.Null(fixture.Lifetime.RetireCanonicalOnly(fixture.Record));
}
private static WorldSession.EntitySpawn Spawn(
uint guid,
ushort incarnation,
bool includePosition = true,
uint setupTableId = 0u,
uint rawState = (uint)(PhysicsStateFlags.Gravity
| PhysicsStateFlags.ReportCollisions),
float? friction = 0.5f,
float? elasticity = 0.05f,
float? translucency = null,
PhysicsMovementData? movement = null)
{
CreateObject.ServerPosition? position = includePosition
? new CreateObject.ServerPosition(Cell, 1f, 2f, 3f, 1f, 0f, 0f, 0f)
: null;
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: incarnation);
var physics = new PhysicsSpawnData(
RawState: rawState,
Position: position,
Movement: movement,
AnimationFrame: null,
SetupTableId: setupTableId == 0u ? null : setupTableId,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: 1f,
Friction: friction,
Elasticity: elasticity,
Translucency: translucency,
Velocity: new Vector3(1f, 2f, 0.5f),
Acceleration: null,
AngularVelocity: new Vector3(0f, 0f, 0.25f),
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
Guid: guid,
Position: position,
SetupTableId: setupTableId == 0u ? null : setupTableId,
AnimPartChanges: Array.Empty<CreateObject.AnimPartChange>(),
TextureChanges: Array.Empty<CreateObject.TextureChange>(),
SubPalettes: Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: 1f,
Name: "remote-entry-fixture",
ItemType: null,
MotionState: null,
MotionTableId: 0x09000001u,
PhysicsState: physics.RawState,
ObjectDescriptionFlags: 0x8u,
Friction: friction,
Elasticity: elasticity,
InstanceSequence: incarnation,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
private sealed class FakeCollisionSource(
uint expectedSetupTableId,
FlatSetupCollision setup) : IPreparedCollisionSource
{
internal int ReadCount { get; private set; }
internal PreparedAssetReadStatus Status { get; set; } =
PreparedAssetReadStatus.Loaded;
public PreparedAssetPresence ProbeCollision(
PakAssetType type, uint sourceFileId) =>
PreparedAssetPresence.Available;
public PreparedCollisionReadResult<FlatSetupCollision> ReadSetupCollision(
uint sourceFileId,
CancellationToken cancellationToken = default)
{
ReadCount++;
Assert.Equal(expectedSetupTableId, sourceFileId);
return Status switch
{
PreparedAssetReadStatus.Loaded =>
PreparedCollisionReadResult<FlatSetupCollision>.Loaded(setup),
PreparedAssetReadStatus.Corrupt =>
PreparedCollisionReadResult<FlatSetupCollision>.Corrupt,
_ => PreparedCollisionReadResult<FlatSetupCollision>.Missing,
};
}
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
ReadGfxObjCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException(
"Only ReadSetupCollision is exercised by these tests.");
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
ReadCellStructureCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException(
"Only ReadSetupCollision is exercised by these tests.");
public PreparedCollisionReadResult<FlatEnvCellTopology>
ReadEnvCellTopology(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException(
"Only ReadSetupCollision is exercised by these tests.");
public PreparedCollisionSourceStats CollisionStats => default;
public void Dispose()
{
}
}
private sealed class Fixture : IDisposable
{
internal Fixture(
bool residentWorld,
uint setupTableId = 0u,
uint rawState = (uint)(PhysicsStateFlags.Gravity
| PhysicsStateFlags.ReportCollisions),
bool isLocalPlayer = false)
{
if (residentWorld)
{
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
engine.AddLandblock(
Landblock,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
Lifetime = new RuntimeEntityObjectLifetime(engine);
}
else
{
Lifetime = new RuntimeEntityObjectLifetime();
}
var generation = new RuntimeGenerationToken(1UL);
Lifetime.BindEventContext(() => generation, static () => 1UL);
// 670f307c: CreateObject frames are landblock-local, so a remote
// conductor's PrepareMover resolves them through Runtime's world
// frame (RuntimeSetPositionState.PrepareMover:1526-1544) and
// returns RetrySetupUnavailable until that frame exists. Only the
// accepted local-player Create publishes it
// (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570 ->
// RuntimePhysicsState.ObserveLocalWorldFrame) — production always
// registers the player before any broadcast Create is driven.
// Model that ambient world fact here, alongside the resident
// terrain above, rather than registering a second entity that
// would perturb every ownership-ledger assertion in this class.
Lifetime.Physics.ObserveLocalWorldFrame(
Cell,
teleportAdvanced: false);
// The SAME conductor instance RuntimeEntityObjectLifetime itself
// constructs and wires into the residence's multicast retirement
// fan-out and BeginSessionClear — never a standalone copy — so
// these tests exercise the real production wiring.
Conductor = Lifetime.RemoteFirstEntry;
Record = Lifetime.RegisterEntityWithInitialResidence(
Spawn(
0x70090002u,
incarnation: 1,
setupTableId: setupTableId,
rawState: rawState),
isLocalPlayer).Canonical!;
Assert.True(Lifetime.TryGetInitialCreateResidence(
Record, out RuntimeInitialCreateResidenceLease lease));
Lease = lease;
CollisionSource = new FakeCollisionSource(
setupTableId,
new FlatSetupCollision(
ImmutableArray<FlatCollisionCylinder>.Empty,
[new FlatCollisionSphere(Vector3.Zero, 0.48f)],
height: 0f,
radius: 0f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f));
}
internal RuntimeEntityObjectLifetime Lifetime { get; }
internal RuntimeRemoteFirstEntryState Conductor { get; }
internal RuntimeEntityRecord Record { get; set; }
internal RuntimeInitialCreateResidenceLease Lease { get; set; }
internal FakeCollisionSource CollisionSource { get; }
internal RuntimeEntityKey Key => Record.Key!.Value;
internal RuntimeRemoteFirstEntryStatus Advance(
out RuntimeInitialCreateExecutionReceipt receipt) =>
Advance(out receipt, out _);
internal RuntimeRemoteFirstEntryStatus Advance(
out RuntimeInitialCreateExecutionReceipt receipt,
out RuntimeRemoteBodyConstructionReceipt construction) =>
Conductor.Advance(
Record,
Lease.Token,
CollisionSource,
gameTime: 10d,
NoContact,
out receipt,
out construction);
public void Dispose() => Lifetime.Dispose();
}
}