acdream/tests/AcDream.Runtime.Tests/Entities/RuntimeRemoteFirstEntryStateTests.cs
Erik 6dc7ba51ee feat(physics): C4 route 4b-3 — remote teleport + cell-less through the canonical placement
Flips the last remote classification (SetPosition: teleport-advanced and
cell-less) onto 4b-1's RuntimeRemotePlacementDriveController, runs retail's
teleport_hook before the placement, and deletes the legacy remote-teleport
machinery. Contract: docs/research/2026-08-04-c4-route-4b-3-contract.md.

Retail: MoveOrTeleport @0x00516330's branch @0x00516386 -> teleport_hook
@0x005163EF -> SetFlags(0x1012) @0x00516414 -> SetPosition @0x00516420 ->
return 1 @0x00516438. The hook @0x00514ED0 runs BEFORE the placement and
regardless of its outcome. Retail places this branch unconditionally, at any
distance and any contact state (arg4 is read only @0x0051638E, after the
branch) — which is what retires AP-137's cell-less enqueue-vs-place delta.

D1 — the classifier's cell-less input is now the PRE-merge committed cell.
Retail's predicate is `this_1->cell == 0`, the BODY's own cell at
MoveOrTeleport entry (this_1 is assigned from this @0x00516334). acdream fed
the POST-merge canonical.FullCellId, which RefreshSnapshot ->
RefreshDerivedState -> SetFullCell has already stamped with the accepted wire
cell; a zero wire cell fails validation into RejectedData first. The shipped
remote cell-less predicate was therefore dead code, not merely different from
remotePlacementRequired. Threaded via a builder overload; route 1's overload
is untouched. The graphical !IsSpatiallyVisible arm of
projectionRequiresTeleportHook is deleted — a presentation predicate with no
retail analogue that fired the teleport machinery on a routine hot path.

Deleted: RemoteTeleportController (605), RemoteTeleportPlacement (85),
RemoteShadowPlacementSynchronizer (49), their 1,709 lines of tests, the
remotePlacementRequired predicate, the TeleportHookRequired plumbing, the
legacy pre-operation ConstrainTo fallback, and the player arm's legacy
!IsGrounded fallback. Net -2,030 lines.

Structural fix (two independent Opus reviews, round 1 FAIL/FAIL): three of the
four MAJORs were one defect — OnPosition carried two parallel inline copies of
the routing tail (player-guid, NPC-guid) that had drifted. Extracted
RunRemoteArmTail (3 call sites) and ApplyWireAirborneLeftoverBookkeeping (2),
both branches now share one implementation.

  A1  ToConstraintArm mapped AirborneSnap -> AirborneNoOperation, so the NPC
      arm armed ConstrainTo ZERO times for an out-of-contact wire-grounded
      creature — a regression this slice introduced while closing a
      structurally identical hole. Now maps to NearInterpolate; switch made
      total with a throwing default proven unreachable.
  R1  D2's write-nothing shape existed on the player arm only; NPC packets
      fell through and wrote the body. Retail makes no player/NPC distinction.
  R2  report_collision_end(this,1) @0x00514F31 was bound to
      ShadowObjects.Suspend, a port of a DIFFERENT retail function
      (remove_shadows_from_cells) that teleport_hook never calls. Now routes
      to RuntimeCollisionReportingState.LeaveWorld, which wraps the private
      ForceEnd in an admission-blocking transaction so a DoCollisionEnd
      callback cannot recreate the contact table.
  R3/A2 A teleported NPC synthesized ServerVelocity from the teleport distance
      (~1,000+ m/s) and planned a run cycle from it. Both the install and
      RemoteServerControlledVelocityCycle.Apply now gate on !isTeleportRoute.

BISECT HAZARD — A1's fix is correct only BECAUSE R1 landed. AirborneSnap is
reachable wire-airborne on the NPC arm only while D2's shape is missing there.
Reverting R1 alone silently inverts A1 into the opposite divergence: arming
where retail returns 0. Revert both or neither.

Also in the velocity hunk: the NPC block's two !IsPlayerGuid(update.Guid)
guards were dropped when it was wrapped in `if (!isTeleportRoute)`. Safe — all
five exit paths of the enclosing IsPlayerGuid block return, so the predicate is
unconditionally false below it — but it was unremarked by both reviews.

Register: AP-137 REWRITTEN (not deleted) to the surviving acdream-only
divergences — null classification during the login window and Rejected*
through UnroutedCatchUp keep a row. AD-42's RemoteTeleportController citation
retired; AP-136/AP-138 writer lists corrected to the two surviving non-Position
rebucket writers; AP-138 gains the teleport arm as a second producer of the
visible-without-collision residual (retirement path remains #309). AP-135 is
untouched and its two airborne bookkeeping writes are preserved on both arms.
AP-131 does not retire; #276 does not close.

Proof obligation 1: ParkCollisionResidents' overlap throw stays unreachable —
the teleport arm adds packets to the same TryBeginExclusiveAuthoredPlacement
one-operation-per-key machinery the far arm uses, opens no new operation shape,
and every DeferredCell outcome cancels synchronously with
restoreCancelledPark: true. The guarded property remains
HasOldPrefixPlacementDebt's stall, not a throw (4b-1's B2 caveat stands).

Correction to an earlier claim: LiveEntityPresentationController's
_activePlacementOwners was NOT write-never at HEAD —
remotePlacementRequired -> BeginPlacement -> Begin -> BeginAuthoritativePlacement
was a live writer chain. It becomes write-never BECAUSE this slice deletes that
chain, which is why deleting the dead half is behaviour-preserving.

Probe: ACDREAM_PROBE_REMOTE_TELEPORT=1 emits one [remote-teleport] line per
routed arm (guid, cause, hook-ran, placement status). TEMPORARY, strip with the
probe family.

Carried, disclosed not fixed: no dedicated bidirectional collision-partner test
for R2 (the wiring, not LeaveWorld itself, is what lacks coverage); the
stress test's teleport step drives hand-written field assignments rather than
the canonical arm; the per-packet runTeleportHook closure allocation (network
path, not the resolve path Slice I's 0 B discipline governs — file before
route 5 adds a fourth call site). B2: IRuntimeCollisionReportObserver has zero
production implementations, so retail's bidirectional DoCollisionEnd half still
reaches no gameplay consumer — this fix closes the wrong-function binding, not
that nobody listens.

Complete Release suite MEASURED at 11,013 passed / 4 skipped / 0 failed
(baseline 11,027/4/0; net -14 = ~33 deleted test cases against ~19 added).
Neither known flake fired (#302 PortalProjectionTests GC-allocation, #308
NakEmissionTests wall-clock).

STILL OWED: the two-client connected gate, which MUST use an NPC/creature
teleport target. Both round-1 MAJORs lived on the NPC arm and the velocity
cycle early-returns for 0x50xxxxxx guids, so a player target structurally
cannot observe A1, A2, or R3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:00:10 +02:00

994 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,
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();
}
}