diff --git a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
index f9c96d00..f9c3f5c9 100644
--- a/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
+++ b/src/AcDream.Runtime/Entities/RuntimeEntityObjectLifetime.cs
@@ -64,7 +64,13 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
/// Advance), but fully constructed/wired like every other owner
/// here, so its own ownership must converge to zero the same way.
///
- int LocalPlayerFirstEntryActiveCount = 0)
+ int LocalPlayerFirstEntryActiveCount = 0,
+ ///
+ /// C3b: outstanding tracked
+ /// keys - the remote/projectile Create-time body-construction conductor.
+ /// Dormant like its C3a sibling; converges to zero the same way.
+ ///
+ int RemoteFirstEntryActiveCount = 0)
{
public bool IsConverged =>
IsDisposed
@@ -87,6 +93,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
&& InitialCreateExecutorProgressCount == 0
&& PendingCompletionReceiptCount == 0
&& LocalPlayerFirstEntryActiveCount == 0
+ && RemoteFirstEntryActiveCount == 0
&& StreamSubscriberCount == 0
&& PlacementStreamSubscriberCount == 0
&& PendingDispatchCount == 0
@@ -176,6 +183,15 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InitialCreateResidences,
InitialCreateExecution,
Physics);
+ // C3b: the remote/projectile analog of the conductor above — retail
+ // body construction at Create time in place of the publication
+ // chain. Dormant like its sibling (no production caller of Advance);
+ // constructed and wired identically so its ownership converges the
+ // same way.
+ RemoteFirstEntry = new RuntimeRemoteFirstEntryState(
+ InitialCreateResidences,
+ InitialCreateExecution,
+ Physics);
// Round 3 B3: every residence retirement path - not only the
// executor's own DiscardProgress calls - must converge the
// executor's progress AND its separately-tracked pending
@@ -190,6 +206,10 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
// updated doc comment for why this is now multicast.
InitialCreateResidences.BindRetirementNotification(
key => LocalPlayerFirstEntry.Forget(key));
+ // C3b: the remote conductor joins the SAME multicast retirement
+ // fan-out, third in registration order.
+ InitialCreateResidences.BindRetirementNotification(
+ key => RemoteFirstEntry.Forget(key));
// F2: reaps the executor's completion-receipt correlation entry
// exactly when a host acknowledges the ExecutorCompleted receipt it
// correlates - mirrors the residence-retirement binding immediately
@@ -245,6 +265,15 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InitialCreateResidences,
InitialCreateExecution,
Physics);
+ // C3b: the remote/projectile analog of the conductor above — retail
+ // body construction at Create time in place of the publication
+ // chain. Dormant like its sibling (no production caller of Advance);
+ // constructed and wired identically so its ownership converges the
+ // same way.
+ RemoteFirstEntry = new RuntimeRemoteFirstEntryState(
+ InitialCreateResidences,
+ InitialCreateExecution,
+ Physics);
// Round 3 B3: every residence retirement path - not only the
// executor's own DiscardProgress calls - must converge the
// executor's progress AND its separately-tracked pending
@@ -259,6 +288,10 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
// updated doc comment for why this is now multicast.
InitialCreateResidences.BindRetirementNotification(
key => LocalPlayerFirstEntry.Forget(key));
+ // C3b: the remote conductor joins the SAME multicast retirement
+ // fan-out, third in registration order.
+ InitialCreateResidences.BindRetirementNotification(
+ key => RemoteFirstEntry.Forget(key));
// F2: reaps the executor's completion-receipt correlation entry
// exactly when a host acknowledges the ExecutorCompleted receipt it
// correlates - mirrors the residence-retirement binding immediately
@@ -314,6 +347,15 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InitialCreateResidences,
InitialCreateExecution,
Physics);
+ // C3b: the remote/projectile analog of the conductor above — retail
+ // body construction at Create time in place of the publication
+ // chain. Dormant like its sibling (no production caller of Advance);
+ // constructed and wired identically so its ownership converges the
+ // same way.
+ RemoteFirstEntry = new RuntimeRemoteFirstEntryState(
+ InitialCreateResidences,
+ InitialCreateExecution,
+ Physics);
// Round 3 B3: every residence retirement path - not only the
// executor's own DiscardProgress calls - must converge the
// executor's progress AND its separately-tracked pending
@@ -328,6 +370,10 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
// updated doc comment for why this is now multicast.
InitialCreateResidences.BindRetirementNotification(
key => LocalPlayerFirstEntry.Forget(key));
+ // C3b: the remote conductor joins the SAME multicast retirement
+ // fan-out, third in registration order.
+ InitialCreateResidences.BindRetirementNotification(
+ key => RemoteFirstEntry.Forget(key));
// F2: reaps the executor's completion-receipt correlation entry
// exactly when a host acknowledges the ExecutorCompleted receipt it
// correlates - mirrors the residence-retirement binding immediately
@@ -353,6 +399,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
internal RuntimeInitialCreateContinuationExecutor InitialCreateExecution
{ get; }
internal RuntimeLocalPlayerFirstEntryState LocalPlayerFirstEntry { get; }
+ internal RuntimeRemoteFirstEntryState RemoteFirstEntry { get; }
public RuntimeEntityObjectOwnershipSnapshot CaptureOwnership()
{
@@ -389,7 +436,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InitialCreateExecution.ReplayFailureCount,
InitialCreateExecution.LastReplayFailure is not null,
InitialCreateExecution.PendingCompletionReceiptCount,
- LocalPlayerFirstEntry.CaptureOwnership().ActiveCount);
+ LocalPlayerFirstEntry.CaptureOwnership().ActiveCount,
+ RemoteFirstEntry.CaptureOwnership().ActiveCount);
}
public void BindEventContext(
@@ -1734,6 +1782,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
InitialCreateResidences.Clear();
InitialCreateExecution.DiscardAll();
LocalPlayerFirstEntry.DiscardAll();
+ RemoteFirstEntry.DiscardAll();
Physics.CollisionReports.LeaveWorldBatch(active);
Physics.ResetSessionPhysics();
Entities.BeginSessionClear();
diff --git a/src/AcDream.Runtime/Entities/RuntimeFirstEntryAcknowledgement.cs b/src/AcDream.Runtime/Entities/RuntimeFirstEntryAcknowledgement.cs
new file mode 100644
index 00000000..10b93734
--- /dev/null
+++ b/src/AcDream.Runtime/Entities/RuntimeFirstEntryAcknowledgement.cs
@@ -0,0 +1,66 @@
+using AcDream.Runtime.Physics;
+
+namespace AcDream.Runtime.Entities;
+
+///
+/// The ONE post-failed-acknowledge re-validation both first-entry conductors
+/// (AcDream.Runtime.Gameplay.RuntimeLocalPlayerFirstEntryState and
+/// ) share. Extracted (C3b review
+/// M2) so the C3a abandonment fix — a delete rewriting the pending Place
+/// slot to Discard with a bumped revision would otherwise make
+/// AcknowledgeProjection fail forever while the conductor retried
+/// endlessly — cannot regress independently in either copy. Follows the
+/// class family's pure-static-helper convention
+/// (,
+/// ,
+/// ).
+///
+internal static class RuntimeFirstEntryAcknowledgement
+{
+ ///
+ /// Re-validates authority after a failed acknowledge. Two independent
+ /// checks, either of which failing means authority moved and the
+ /// conductor must abandon rather than keep retrying forever:
+ /// (1) the residence lease the whole sequence began under must still be
+ /// exactly current (a delete or reset retires it); (2) if the FIFO head
+ /// belongs to THIS entity at all, it must still be the exact Place
+ /// projection the conductor is holding — a head that belongs to us but
+ /// is no longer that exact token (rewritten to Discard, or to a later
+ /// revision) means our specific placement was superseded even if the
+ /// residence lookup transiently still resolves. A head belonging to a
+ /// DIFFERENT entity is the genuine "not yet our turn" case and must
+ /// stay retryable. This reads the Runtime-internal
+ /// directly
+ /// rather than through the public, generation-gated
+ /// — the conductors are
+ /// part of Runtime, not external hosts crossing that boundary, exactly
+ /// like their existing direct
+ /// calls.
+ ///
+ internal static bool IsStillPending(
+ RuntimeInitialCreateResidenceState residences,
+ RuntimeSetPositionState setPosition,
+ RuntimeEntityRecord record,
+ in RuntimeInitialCreateResidenceToken residenceToken,
+ in RuntimePlacementProjectionToken expected)
+ {
+ if (!residences.TryGetCurrent(
+ record,
+ out RuntimeInitialCreateResidenceLease lease)
+ || lease.Token != residenceToken)
+ {
+ return false;
+ }
+
+ if (setPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot head)
+ && head.Token.Entity == expected.Entity
+ && (head.Kind is not RuntimePlacementProjectionKind.Place
+ || head.Token != expected))
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/AcDream.Runtime/Entities/RuntimeRemoteBodyDescription.cs b/src/AcDream.Runtime/Entities/RuntimeRemoteBodyDescription.cs
new file mode 100644
index 00000000..b2335135
--- /dev/null
+++ b/src/AcDream.Runtime/Entities/RuntimeRemoteBodyDescription.cs
@@ -0,0 +1,321 @@
+using System.Numerics;
+using AcDream.Core.Net.Messages;
+using AcDream.Core.Physics;
+using AcDream.Runtime.Physics;
+
+namespace AcDream.Runtime.Entities;
+
+///
+/// Typed record of one retail-ordered body construction. Fields with no
+/// slot (translucency) or no unconditional retail
+/// write (the three float gates) are recorded here so the construction's
+/// gated outcomes stay provable both ways without a parallel body field.
+/// The receipt is Runtime-internal evidence, never a second source of truth:
+/// the body's own fields remain authoritative for everything they carry.
+///
+internal readonly record struct RuntimeRemoteBodyConstructionReceipt(
+ /// The motion-table id retail's gate evaluated (0 = none on the wire).
+ uint MotionTableId,
+ ///
+ /// Retail CPhysicsObj::SetMotionTableID (0x00512780,
+ /// pseudo-C:280528) fails ONLY when part_array == 0 (005127da) or
+ /// when MotionTableManager::Create fails for a NONZERO id
+ /// (CPartArray::SetMotionTableID 0x005186E0, pseudo-C:286732,
+ /// 0051872f). A ZERO id skips manager creation (0051871f falls through
+ /// to return 1 at 00518743) and CPhysicsObj then skips
+ /// MakeMovementManager for INVALID_DID (005127ca) — the gate
+ /// PASSES. In Runtime the part-array precondition is the already-run
+ /// mover-preparation stage (the Setup shape resolved before this
+ /// construction is reachable) and motion-table DAT installation stays
+ /// presentation-side, so the gate passes for zero and nonzero ids alike;
+ /// this field records that it was evaluated in retail's position.
+ ///
+ bool MotionTableGatePassed,
+ ///
+ /// True when the frozen PhysicsDesc carried a Movement payload —
+ /// retail's mutually-exclusive branch (set_description step 4,
+ /// 0x00514F40): movement present means NO placement frame is staged.
+ ///
+ bool MovementBranch,
+ /// Retail last_move_was_autonomous written on the movement branch.
+ bool LastMoveWasAutonomous,
+ /// True when the no-movement branch staged the dormant cell frame.
+ bool PlacementFrameStaged,
+ /// The friction value the body carries after the gated write.
+ float Friction,
+ ///
+ /// Friction gate outcome — byte-certain gates 1+2 of
+ /// docs/research/2026-08-02-set-description-float-gates.md: applied only
+ /// when 0.0f <= friction <= 1.0f.
+ ///
+ bool FrictionApplied,
+ /// The elasticity the body carries after retail set_elasticity's clamp.
+ float Elasticity,
+ ///
+ /// Retail's UNCONDITIONAL translucencyOriginal write
+ /// (0x00515097, gates doc conditional 3 — written before the gate,
+ /// always). has no translucency slot
+ /// (translucency is presentation-side in acdream — the AP-89
+ /// TranslucencyFadeManager family), so the construction receipt is where
+ /// the unconditional write lands Runtime-side.
+ ///
+ float TranslucencyOriginal,
+ ///
+ /// Translucency gate outcome — byte-certain gate 3: live translucency
+ /// (+ PartArray propagation, presentation-side) only when
+ /// translucency != 0.0f.
+ ///
+ bool TranslucencyApplied,
+ /// True when a present, finite wire velocity was applied via set_velocity.
+ bool VelocityApplied,
+ /// True when a present, finite wire omega was written (raw field, no setter).
+ bool OmegaApplied);
+
+///
+/// Pure retail-ordered construction of one remote/projectile canonical
+/// from the frozen wire ,
+/// per CPhysicsObj::set_description (0x00514F40, pseudo-C:283155-283276)
+/// as called from ACCObjectMaint::CreateObject (0x00558870,
+/// pseudo-C:356155-356245, step 6). The caller
+/// () sequences this AFTER
+/// mover preparation (retail: CPhysicsObj::makeObject shapes the
+/// Setup/part-array before set_description runs) and binds the result through
+/// the canonical
+/// writer — never a parallel binding idiom.
+///
+/// Field routing, per the retail order:
+///
+/// - Motion-table gate — evaluated (see
+/// );
+/// motion-table DAT installation stays presentation-side.
+/// - Sound table (step 2) / physics-script table (step 3) — snapshot/
+/// presentation-side today; not body fields.
+/// - Placement-frame-vs-Movement (step 4, mutually exclusive) — movement
+/// payload present writes
+/// (retail last_move_was_autonomous = get_autonomous_movement) and
+/// stages NO frame (movement unpack itself is presentation-side today);
+/// no payload stages the dormant cell frame from the PREPARED MOVER
+/// COMMAND's exact position (retail
+/// SetPlacementFrameInternal; enter_world remains the later
+/// submission — stays false).
+/// - set_state (step 5) — from
+/// , the retail
+/// state-transition view of the wire state (mirrors the existing canonical
+/// writers' convention: RuntimePhysicsState.InitializeNewPhysicsBody,
+/// SetRemoteMotion).
+/// - Scale (step 6) — NOT a field; it rides
+/// the prepared mover command
+/// ( reads
+/// Snapshot.Physics.Scale ?? ObjScale ?? 1f, matching the
+/// PhysicsDesc constructor default 1f at 0x0051D4D0).
+/// - Friction (step 7) — gated per the byte-certain gates doc.
+/// - Elasticity (step 8) — via the set_elasticity clamp port below.
+/// - Translucency (step 9) — original always recorded; live apply gated;
+/// both receipt-side (no body slot; presentation owns render alpha).
+/// - set_velocity (step 10) / omega raw write (step 11) — retail passes
+/// the desc values unconditionally, but the desc DEFAULTS are the zero
+/// vector (PhysicsDesc ctor 0x0051D4D0 / Destroy 0x0051D5D0), so gating on
+/// wire presence — the existing InitializeNewPhysicsBody
+/// convention, mirrored here — produces the identical end state for an
+/// absent field (the fresh body's velocity/omega are already zero).
+/// - default_script / default_script_intensity (step 12) — snapshot/
+/// presentation-side; not body fields.
+/// - All nine PhysicsTimeStamp slots (step 13, LAST) — already applied
+/// at admission and frozen on the residence lease
+/// (); nothing body-side.
+///
+///
+internal static class RuntimeRemoteBodyDescription
+{
+ ///
+ /// PhysicsDesc constructor default friction: 0x0051D4D0 (pseudo-C:292056)
+ /// writes bytes "33s?" = 0x3F733333 = 0.95f. Same value as
+ /// .
+ ///
+ private const float DefaultDescFriction = 0.95f;
+
+ /// PhysicsDesc constructor default elasticity (0x0051D4D0): 0.05f.
+ private const float DefaultDescElasticity = 0.05f;
+
+ ///
+ /// Retail CPhysicsObj::set_elasticity (0x0050FD40,
+ /// pseudo-C:277817) upper clamp constant 0.100000001f (float 0.1).
+ /// Cross-check: ACE PhysicsGlobals.MaxElasticity = 0.1f
+ /// (ACE PhysicsObj.cs:3586-3599 reproduces the identical clamp).
+ ///
+ private const float MaxElasticity = 0.1f;
+
+ internal static PhysicsBody Construct(
+ RuntimeEntityRecord record,
+ PhysicsSpawnData? description,
+ in RuntimeSetPositionCommand preparedCommand,
+ out RuntimeRemoteBodyConstructionReceipt receipt)
+ {
+ ArgumentNullException.ThrowIfNull(record);
+ var body = new PhysicsBody();
+
+ // 1. Motion-table gate — set_description step 1 (0x00514F5C,
+ // pseudo-C:283159): the ONLY group that gates the rest of the
+ // function. See the receipt field's doc comment for the recovered
+ // zero-id semantics; in Runtime the gate passes (part-array analog
+ // already satisfied by the sequenced mover preparation; DAT
+ // motion-table installation is presentation-side).
+ uint motionTableId = description?.MotionTableId ?? 0u;
+ const bool motionTableGatePassed = true;
+
+ // 2./3. Sound table + physics-script table (steps 2-3) — snapshot/
+ // presentation-side; deliberately untouched here.
+
+ // 4. Placement-frame-vs-movement — mutually exclusive
+ // (set_description step 4, 0x00514F40; load-bearing ordering fact 2
+ // of the retail notes). C3b retail review R1: the retail
+ // discriminator is the movement BUFFER pointer (`movement_buffer !=
+ // 0`), NOT the wire flag — PhysicsDesc::UnPack (0x0051DDD0) assigns
+ // movement_buffer only inside its `if (buff_length != 0)` block
+ // (0051DE1F-0051DE2A) after Destroy nulled it (0051D61A), so a
+ // movement-flag-set-with-EMPTY-buffer desc reaches set_description
+ // with movement_buffer == 0 and takes the PLACEMENT branch. The
+ // autonomy write lives in the ELSE (movement) branch only — retail's
+ // empty-buffer flavor writes neither the frame-suppression NOR
+ // last_move_was_autonomous (UnPack likewise reads
+ // autonomous_movement only inside the same nonzero-length block, so
+ // an empty-buffer desc never even carries a wire autonomy value).
+ // Our parser materializes a non-null PhysicsMovementData wrapper
+ // with empty RawData for exactly that case, so the wrapper's
+ // presence alone must never decide the branch.
+ bool movementBranch = description?.Movement is { } movementData
+ && !movementData.RawData.IsEmpty;
+ bool lastMoveWasAutonomous = false;
+ bool placementFrameStaged = false;
+ if (movementBranch)
+ {
+ // Retail: this->last_move_was_autonomous =
+ // PhysicsDesc::get_autonomous_movement(esi) — written whenever a
+ // movement payload is present, before the createMode-gated
+ // unpack_movement (which is presentation-side today).
+ lastMoveWasAutonomous =
+ description!.Value.Movement!.Value.IsAutonomous ?? false;
+ body.LastMoveWasAutonomous = lastMoveWasAutonomous;
+ }
+ else
+ {
+ // Retail: CPhysicsObj::SetPlacementFrameInternal — direct
+ // placement from the description's position frame. The staged
+ // frame uses the PREPARED MOVER COMMAND's exact values (the same
+ // accepted wire position the submission will resolve), so there
+ // is no second position source. enter_world remains the later
+ // submission suffix — InWorld stays false
+ // (PhysicsBody.StageDormantCellFrame's documented contract).
+ body.Orientation = preparedCommand.Physics.Orientation;
+ body.StageDormantCellFrame(
+ preparedCommand.Physics.CellId,
+ preparedCommand.Physics.Position,
+ preparedCommand.Physics.CellLocalPosition);
+ placementFrameStaged = true;
+ }
+
+ // 5. set_state — unconditional, AFTER the position/movement branch
+ // (set_description step 5). FinalPhysicsState is the retail
+ // state-transition view of the wire state; assigning it mirrors
+ // every existing canonical writer.
+ body.State = record.FinalPhysicsState;
+
+ // 6. Scale — command-side (see the class doc comment); no body slot.
+
+ // 7. Friction — byte-certain gates 1+2
+ // (docs/research/2026-08-02-set-description-float-gates.md):
+ // outer FCOM vs 0.0 (VA 0x0051505A) admits friction >= 0.0f; inner
+ // FCOM vs 1.0 (VA 0x0051506A) admits friction <= 1.0f; only then is
+ // this->friction assigned (0x0051506C). An absent wire friction
+ // takes the PhysicsDesc constructor default 0.95f, which passes the
+ // gate — identical to the fresh body's own default, applied
+ // explicitly to keep retail's write order observable.
+ // NaN (C3b retail review R2b): retail's x87 unordered case falls
+ // into the APPLY bucket on both compares — the gates doc's
+ // pre-declared accepted compiler-codegen quirk ("not something the
+ // retail struct's float fields would ever hit in practice"). This
+ // modern port's `>= && <=` deliberately SKIPS NaN (both comparisons
+ // fail), the doc's sanctioned deviation; keep it — do not "fix" it
+ // toward the quirk.
+ float friction = description?.Friction ?? DefaultDescFriction;
+ bool frictionApplied = friction >= 0.0f && friction <= 1.0f;
+ if (frictionApplied)
+ body.Friction = friction;
+
+ // 8. Elasticity — unconditional via the setter (set_description step
+ // 8); the CLAMP lives inside CPhysicsObj::set_elasticity 0x0050FD40:
+ // < 0 -> 0; <= 0.1 -> value; > 0.1 -> 0.1 (ACE MaxElasticity = 0.1f
+ // agrees; the <= vs < boundary difference at exactly 0.1 is
+ // valueless — both assign 0.1).
+ // NaN (C3b retail review R2a): retail's FIRST x87 compare sends the
+ // unordered case into the zeroing arm (0x0050FD51) — NaN -> 0f.
+ // `!(e >= 0f)` reproduces that exactly (NaN fails every ordered
+ // comparison, so it lands in the first arm like retail; a plain
+ // `e < 0f` would instead fall through and clamp NaN to 0.1f, which
+ // is ACE's — divergent — behavior, not the binary's).
+ float elasticity = description?.Elasticity ?? DefaultDescElasticity;
+ body.Elasticity = !(elasticity >= 0f)
+ ? 0f
+ : elasticity <= MaxElasticity
+ ? elasticity
+ : MaxElasticity;
+
+ // 9. Translucency — translucencyOriginal is ALWAYS written
+ // (0x00515097, before the gate); the LIVE apply + PartArray
+ // propagation happen only when translucency != 0.0f (byte-certain
+ // gate 3, JNP fires ONLY for exact equality with 0.0f). No body
+ // slot — recorded on the receipt; render alpha stays
+ // presentation-side. NaN: `!=` sends NaN into the apply bucket,
+ // which here MATCHES retail's unordered case exactly (gates doc
+ // conditional 3's case table) — no deviation to manage.
+ // R2c note: IsFinite guards remain velocity/omega-only (the
+ // existing InitializeNewPhysicsBody convention); the float trio's
+ // NaN routes are each pinned above instead — no broader validation.
+ float translucency = description?.Translucency ?? 0f;
+ bool translucencyApplied = translucency != 0.0f;
+
+ // 10. set_velocity — via the setter (set_description step 10).
+ // Presence-gated per the InitializeNewPhysicsBody convention; the
+ // absent-field end state is identical (desc default zero vector).
+ bool velocityApplied = false;
+ if (description?.Velocity is { } velocity && IsFinite(velocity))
+ {
+ body.set_velocity(velocity);
+ velocityApplied = true;
+ }
+
+ // 11. Omega — DIRECT field write, not the setter (load-bearing
+ // ordering fact 4 of the retail notes: velocity and omega are not
+ // symmetric).
+ bool omegaApplied = false;
+ if (description?.AngularVelocity is { } omega && IsFinite(omega))
+ {
+ body.Omega = omega;
+ omegaApplied = true;
+ }
+
+ // 12. default_script / default_script_intensity — snapshot-side.
+ // 13. All nine timestamps — LAST in retail; already applied at
+ // admission and frozen on the residence lease; nothing body-side.
+
+ receipt = new RuntimeRemoteBodyConstructionReceipt(
+ motionTableId,
+ motionTableGatePassed,
+ movementBranch,
+ lastMoveWasAutonomous,
+ placementFrameStaged,
+ body.Friction,
+ frictionApplied,
+ body.Elasticity,
+ translucency,
+ translucencyApplied,
+ velocityApplied,
+ omegaApplied);
+ return body;
+ }
+
+ private static bool IsFinite(Vector3 value) =>
+ float.IsFinite(value.X)
+ && float.IsFinite(value.Y)
+ && float.IsFinite(value.Z);
+}
diff --git a/src/AcDream.Runtime/Entities/RuntimeRemoteFirstEntryState.cs b/src/AcDream.Runtime/Entities/RuntimeRemoteFirstEntryState.cs
new file mode 100644
index 00000000..9a8ae39e
--- /dev/null
+++ b/src/AcDream.Runtime/Entities/RuntimeRemoteFirstEntryState.cs
@@ -0,0 +1,649 @@
+using AcDream.Content;
+using AcDream.Core.Physics;
+using AcDream.Runtime.Physics;
+
+namespace AcDream.Runtime.Entities;
+
+///
+/// Typed yields for .
+/// Mirrors the C3a conductor's vocabulary
+/// (RuntimeLocalPlayerFirstEntryStatus) rather than inventing a
+/// parallel one; the two publication-only statuses have no remote analog.
+///
+internal enum RuntimeRemoteFirstEntryStatus : byte
+{
+ ///
+ /// The underlying
+ /// call reported Completed: residence consumed, initial tail and
+ /// FIFO drained, ExecutorCompleted receipt dispatched. Terminal.
+ ///
+ Completed,
+
+ ///
+ /// The authored-mover Setup read
+ /// () is not
+ /// yet available. Retry with the same arguments once the prepared-asset
+ /// package lands; no Runtime state changed.
+ ///
+ AwaitingCollisionSource,
+
+ ///
+ /// The submitted placement deferred (DeferredCell — destination
+ /// collision generation not ready, or a collision-prefix quiescence held
+ /// it) and its parked operation has not produced an acknowledgeable
+ /// Place projection yet. The wake is internal to
+ /// (collision-generation commit
+ /// drives RetryDeferred); retry after it.
+ ///
+ AwaitingPlacement,
+
+ ///
+ /// Our projection exists but could not be acknowledged this call —
+ /// either another entity's receipt sits ahead of ours in the one ordered
+ /// FIFO, or
+ /// still observed PendingPlacement. Retry the same stage.
+ ///
+ AwaitingReceiptAcknowledgement,
+
+ ///
+ /// Passthrough of the executor's own AwaitingContinuationPlacement
+ /// — a later FIFO continuation needs its own authored placement before
+ /// the drain can finish; entirely the executor's concern from here on.
+ ///
+ AwaitingContinuationPlacement,
+
+ ///
+ /// A reentrant for the SAME entity arrived while an
+ /// outer call for it was still on the stack, or another owner's
+ /// body/remote-motion binding callback is mid-flight on this record.
+ /// Retry once the outer call has returned.
+ ///
+ Contention,
+
+ ///
+ /// The residence token matches nothing this conductor can own — including
+ /// a LOCAL-PLAYER lease (),
+ /// which belongs to the C3a conductor, never this one.
+ ///
+ RejectedToken,
+
+ ///
+ /// An authority-shaped failure (stale epoch/session/identity, deleted or
+ /// replaced record, a foreign physics body bound out-of-band, a rejected
+ /// or cancelled submission). Abandoned; progress removed. The caller must
+ /// begin a fresh sequence (a new residence lease), never retry this call.
+ ///
+ RejectedAuthority,
+}
+
+internal readonly record struct RuntimeRemoteFirstEntryOwnershipSnapshot(
+ int ActiveCount)
+{
+ internal bool IsConverged => ActiveCount == 0;
+}
+
+///
+/// The dormant, resumable Runtime transaction that dissolves C3's Finding C:
+/// ordinary remote-creature and projectile Creates classify to
+/// SetPosition, but no production path constructs their canonical
+/// at Create time (bodies arrive with first motion
+/// today), so 's
+/// Record.PhysicsBody requirement rejects the residence route's
+/// initial placement. This class is the remote analog of the C3a conductor
+/// (RuntimeLocalPlayerFirstEntryState) WITHOUT the publication chain —
+/// remotes have no PlayerMovementController — and with retail body
+/// construction in its place:
+///
+/// mover preparation (retail CPhysicsObj::makeObject shaping the
+/// Setup, which precedes set_description in
+/// ACCObjectMaint::CreateObject 0x00558870 step 2-vs-6) ->
+/// body construction per the exact set_description order
+/// (0x00514F40; ) bound through the
+/// canonical writer
+/// -> ordinary authored submission
+/// ( — retail
+/// enter_world, which SmartBox::HandleCreateObject 0x00454C80
+/// runs for a top-level object with a nonzero wire cell AFTER CreateObject
+/// returns) -> Withdraw/Place receipt acknowledgement ->
+/// (FIFO
+/// drain).
+///
+/// Unlike the local-player path this class never touches the dormant
+/// activation family: no operation it drives ever has
+/// DormantLocalActivation set, so the ordinary submission tail is the
+/// correct — and only — commit route.
+///
+/// Dormant by design: fully
+/// constructs and wires this class (construction, retirement fan-out, bulk
+/// session-clear cleanup, ownership fold) exactly like the C3a conductor,
+/// but nothing calls in production — C3c wires the
+/// hosts.
+///
+internal sealed class RuntimeRemoteFirstEntryState
+{
+ private enum Stage : byte
+ {
+ /// No progress yet, or the mover has not been prepared.
+ AwaitingMoverPreparation,
+
+ /// Mover command in hand; the body has not been constructed.
+ MoverPrepared,
+
+ ///
+ /// The canonical body is constructed and bound; the placement has
+ /// not been submitted.
+ ///
+ BodyConstructed,
+
+ ///
+ /// Submission deferred (DeferredCell): the parked operation's
+ /// Withdraw/Place receipts are drained from the projection FIFO as
+ /// they surface; the wake itself is internal to
+ /// .
+ ///
+ PlacementSubmitted,
+
+ ///
+ /// The Place projection token is known but not yet acknowledged.
+ ///
+ PlacementCommitted,
+
+ ///
+ /// The Place projection has been acknowledged. Only
+ ///
+ /// remains; the acknowledgement step is never re-entered.
+ ///
+ Acknowledged,
+ }
+
+ private sealed class Progress
+ {
+ internal required ulong LeaseId { get; init; }
+ internal Stage Stage { get; set; } = Stage.AwaitingMoverPreparation;
+ internal RuntimeSetPositionCommand PreparedCommand { get; set; }
+ internal PhysicsBody? ConstructedBody { get; set; }
+ internal RuntimeRemoteBodyConstructionReceipt Construction { get; set; }
+ internal RuntimePlacementProjectionToken Projection { get; set; }
+ }
+
+ private readonly RuntimeInitialCreateResidenceState _residences;
+ private readonly RuntimeInitialCreateContinuationExecutor _executor;
+ private readonly RuntimePhysicsState _physics;
+ private readonly Dictionary _progress = [];
+ private readonly HashSet _executing = [];
+
+ internal RuntimeRemoteFirstEntryState(
+ RuntimeInitialCreateResidenceState residences,
+ RuntimeInitialCreateContinuationExecutor executor,
+ RuntimePhysicsState physics)
+ {
+ _residences = residences
+ ?? throw new ArgumentNullException(nameof(residences));
+ _executor = executor
+ ?? throw new ArgumentNullException(nameof(executor));
+ _physics = physics
+ ?? throw new ArgumentNullException(nameof(physics));
+ }
+
+ ///
+ /// Exposes the body-construction receipt for a still-tracked entry —
+ /// the MID-FLIGHT half of the consumption rule documented on
+ /// (C3b review M1): while the sequence is in
+ /// flight this query serves diagnostics/tests; the terminal
+ /// Completed yield delivers the same receipt through Advance's
+ /// own out-param in the call that reaps this entry. Returns false once
+ /// the sequence completed or was abandoned.
+ ///
+ internal bool TryGetConstruction(
+ RuntimeEntityKey key,
+ out RuntimeRemoteBodyConstructionReceipt construction)
+ {
+ if (_progress.TryGetValue(key, out Progress? progress)
+ && progress.ConstructedBody is not null)
+ {
+ construction = progress.Construction;
+ return true;
+ }
+ construction = default;
+ return false;
+ }
+
+ ///
+ /// One resumable step. Callers pass the SAME arguments on every retry;
+ /// this method re-reads currency from the owning states on every entry
+ /// rather than trusting anything cached beyond its own stage cursor and
+ /// the exact command/token structs the owning methods themselves require.
+ ///
+ /// Construction-receipt consumption rule (C3b review M1),
+ /// following the C3a/F2 precedent of receipts riding the terminal
+ /// Advance out-params: is populated
+ /// ONLY on the
+ /// yield — the same call that delivers the executor
+ /// — because the terminal Advance is a C3c
+ /// host's one natural consumption point and the progress entry (the
+ /// receipt's only retained storage) is reaped in that same call.
+ /// Mid-flight the receipt stays inspectable via
+ /// ; after Completed nothing is
+ /// retained. A lease whose route performs no SetPosition (Parented/
+ /// PickedUp) constructs no body, so its terminal receipt is default.
+ ///
+ internal RuntimeRemoteFirstEntryStatus Advance(
+ RuntimeEntityRecord record,
+ in RuntimeInitialCreateResidenceToken residenceToken,
+ IPreparedCollisionSource collisionSource,
+ double gameTime,
+ in RuntimeInitialCreateExecutionInputs inputs,
+ out RuntimeInitialCreateExecutionReceipt receipt,
+ out RuntimeRemoteBodyConstructionReceipt construction)
+ {
+ ArgumentNullException.ThrowIfNull(record);
+ ArgumentNullException.ThrowIfNull(collisionSource);
+ receipt = default;
+ construction = default;
+ if (!residenceToken.IsValid || record.Key is not { } key)
+ return RuntimeRemoteFirstEntryStatus.RejectedToken;
+
+ // Mirrors the executor's and the C3a conductor's _executing guard: a
+ // synchronous reentrant call for the SAME entity fails closed rather
+ // than interleaving two drains of one stage machine.
+ if (!_executing.Add(key))
+ return RuntimeRemoteFirstEntryStatus.Contention;
+ try
+ {
+ return AdvanceCore(
+ record,
+ residenceToken,
+ collisionSource,
+ gameTime,
+ inputs,
+ key,
+ out receipt,
+ out construction);
+ }
+ finally
+ {
+ _executing.Remove(key);
+ }
+ }
+
+ private RuntimeRemoteFirstEntryStatus AdvanceCore(
+ RuntimeEntityRecord record,
+ in RuntimeInitialCreateResidenceToken residenceToken,
+ IPreparedCollisionSource collisionSource,
+ double gameTime,
+ in RuntimeInitialCreateExecutionInputs inputs,
+ RuntimeEntityKey key,
+ out RuntimeInitialCreateExecutionReceipt receipt,
+ out RuntimeRemoteBodyConstructionReceipt construction)
+ {
+ receipt = default;
+ construction = default;
+
+ _progress.TryGetValue(key, out Progress? progress);
+ // ABA/GUID-reuse guard, exactly like the C3a conductor and the
+ // executor's own Progress reconciliation.
+ if (progress is not null && progress.LeaseId != residenceToken.LeaseId)
+ {
+ Discard(key);
+ progress = null;
+ }
+
+ if (progress is null || progress.Stage is Stage.AwaitingMoverPreparation)
+ {
+ if (!_residences.TryGetCurrent(
+ record,
+ out RuntimeInitialCreateResidenceLease lease)
+ || lease.Token != residenceToken)
+ {
+ if (progress is null)
+ return RuntimeRemoteFirstEntryStatus.RejectedToken;
+ Discard(key);
+ return RuntimeRemoteFirstEntryStatus.RejectedAuthority;
+ }
+
+ // This conductor owns REMOTE and PROJECTILE residence leases
+ // only. A local-player lease (InitialLogin — or the structurally
+ // impossible-at-Create LocalAuthoritative) belongs to the C3a
+ // conductor and its publication chain; refusing it here is
+ // "nothing tracked in this domain", not an abandonment.
+ if (lease.Route.OperationKind
+ is not (RuntimeSetPositionOperationKind.RemoteAuthoritative
+ or RuntimeSetPositionOperationKind.ProjectileAuthoritative))
+ {
+ return RuntimeRemoteFirstEntryStatus.RejectedToken;
+ }
+
+ if (!lease.Route.PerformsSetPosition)
+ {
+ // Parented/PickedUp residence: no SetPosition operation
+ // exists, so there is nothing to place and — matching
+ // today's production behavior for those routes — no body is
+ // constructed at Create (retail constructs one, but a
+ // parented child's placement is driven by later parent/
+ // pickup events; body-at-Create for those routes stays with
+ // the first-motion path until a later slice widens this).
+ // Skip straight to Execute, mirroring the C3a conductor.
+ progress ??= new Progress { LeaseId = residenceToken.LeaseId };
+ progress.Stage = Stage.Acknowledged;
+ _progress[key] = progress;
+ return RunExecute(
+ record,
+ residenceToken,
+ inputs,
+ key,
+ progress,
+ out receipt,
+ out construction);
+ }
+
+ RuntimeSetPositionMoverPreparationStatus moverStatus = _physics
+ .SetPosition.TryPrepareAuthoredMover(
+ record,
+ lease.Placement,
+ lease.Route.OperationKind,
+ lease.Route.SetPositionFlags,
+ collisionSource,
+ gameTime,
+ out RuntimeSetPositionCommand command);
+ if (moverStatus
+ == RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable)
+ {
+ return RuntimeRemoteFirstEntryStatus.AwaitingCollisionSource;
+ }
+ if (moverStatus != RuntimeSetPositionMoverPreparationStatus.Prepared)
+ {
+ if (progress is not null)
+ Discard(key);
+ return RuntimeRemoteFirstEntryStatus.RejectedAuthority;
+ }
+
+ progress ??= new Progress { LeaseId = residenceToken.LeaseId };
+ progress.PreparedCommand = command;
+ progress.Stage = Stage.MoverPrepared;
+ _progress[key] = progress;
+ }
+
+ if (progress.Stage is Stage.MoverPrepared)
+ {
+ if (!_residences.TryGetCurrent(
+ record,
+ out RuntimeInitialCreateResidenceLease lease)
+ || lease.Token != residenceToken)
+ {
+ Discard(key);
+ return RuntimeRemoteFirstEntryStatus.RejectedAuthority;
+ }
+
+ if (record.PhysicsBody is { } existing)
+ {
+ if (ReferenceEquals(progress.ConstructedBody, existing))
+ {
+ // Idempotent retry: our own construction already bound.
+ progress.Stage = Stage.BodyConstructed;
+ }
+ else
+ {
+ // A body this conductor did not construct appeared while
+ // the residence lease was still active — an out-of-band
+ // owner raced Create-time construction. Never clobber an
+ // existing canonical body (the writer map's invariant);
+ // fail closed and let the lease's own retirement path
+ // converge.
+ Discard(key);
+ return RuntimeRemoteFirstEntryStatus.RejectedAuthority;
+ }
+ }
+ else if (record.PhysicsBodyAcquisitionInProgress
+ || record.RemoteMotionBindingInProgress)
+ {
+ // Another owner's binding callback is mid-flight on this
+ // exact record (only reachable when this Advance itself runs
+ // inside that callback). Typed contention instead of letting
+ // GetOrCreatePhysicsBody throw its structural guard.
+ return RuntimeRemoteFirstEntryStatus.Contention;
+ }
+ else
+ {
+ // Retail order: CreateObject acquires the physics object
+ // from the Setup (makeObject — our mover preparation, stage
+ // 1) and then applies the PhysicsDesc via set_description
+ // (RuntimeRemoteBodyDescription.Construct). Binding runs
+ // through the canonical GetOrCreatePhysicsBody writer: its
+ // post-factory InitializeNewPhysicsBody re-applies
+ // state/velocity/omega from the live snapshot — identical by
+ // value to the frozen-description writes the factory already
+ // made (nothing between admission and this call mutates the
+ // snapshot's physics payload; continuations are queued, not
+ // applied) — and its SynchronizeBodyActiveState aligns the
+ // Active transient bit with the record's object clock.
+ RuntimeRemoteBodyConstructionReceipt built = default;
+ PhysicsBody constructed = _physics.GetOrCreatePhysicsBody(
+ record,
+ r => RuntimeRemoteBodyDescription.Construct(
+ r,
+ lease.InitialCreate.Physics,
+ progress.PreparedCommand,
+ out built));
+ progress.ConstructedBody = constructed;
+ progress.Construction = built;
+ progress.Stage = Stage.BodyConstructed;
+ }
+ }
+
+ if (progress.Stage is Stage.BodyConstructed)
+ {
+ if (!_residences.TryGetCurrent(
+ record,
+ out RuntimeInitialCreateResidenceLease lease)
+ || lease.Token != residenceToken)
+ {
+ Discard(key);
+ return RuntimeRemoteFirstEntryStatus.RejectedAuthority;
+ }
+
+ RuntimeSetPositionOutcome outcome = _physics.SetPosition
+ .SubmitPreparedPlacement(lease.Placement, progress.PreparedCommand);
+ switch (outcome.Status)
+ {
+ case RuntimeSetPositionStatus.CommittedHostAcknowledgementPending:
+ progress.Projection = outcome.Projection;
+ progress.Stage = Stage.PlacementCommitted;
+ break;
+ case RuntimeSetPositionStatus.DeferredCell:
+ // ParkDeferred published a Withdraw receipt and parked
+ // the operation; the projection FIFO drives everything
+ // from here (drained in the PlacementSubmitted stage
+ // below, this same call).
+ progress.Stage = Stage.PlacementSubmitted;
+ break;
+ default:
+ // Rejected (the SetPosition transaction failed — retail's
+ // enter_world failure leaves the object celless; the
+ // resident-cell-cleanup family owns that destiny, not a
+ // silent retry here) or Cancelled (a reentrant observer
+ // displaced the operation). Fail closed.
+ Discard(key);
+ return RuntimeRemoteFirstEntryStatus.RejectedAuthority;
+ }
+ }
+
+ if (progress.Stage is Stage.PlacementSubmitted)
+ {
+ if (!_residences.TryGetCurrent(
+ record,
+ out RuntimeInitialCreateResidenceLease lease)
+ || lease.Token != residenceToken)
+ {
+ Discard(key);
+ return RuntimeRemoteFirstEntryStatus.RejectedAuthority;
+ }
+
+ // Drain OUR OWN receipts from the FIFO head as they surface:
+ // Withdraw (the deferred park) must be acknowledged before the
+ // internal collision-generation wake can resubmit; the wake's
+ // commit then publishes the Place this stage is waiting for.
+ while (true)
+ {
+ if (!_physics.SetPosition.TryPeekProjection(
+ out RuntimePlacementProjectionSnapshot head))
+ {
+ // Nothing pending anywhere — the operation is parked
+ // awaiting its cell/collision-generation wake. The
+ // residence currency check above already proved the
+ // placement operation itself is still tracked.
+ return RuntimeRemoteFirstEntryStatus.AwaitingPlacement;
+ }
+ if (head.Token.Entity != key)
+ {
+ // Another entity's receipt sits ahead of ours in the one
+ // ordered FIFO.
+ return RuntimeRemoteFirstEntryStatus
+ .AwaitingReceiptAcknowledgement;
+ }
+ if (head.Kind is RuntimePlacementProjectionKind.Withdraw)
+ {
+ if (!_physics.SetPosition.AcknowledgeProjection(head.Token))
+ {
+ Discard(key);
+ return RuntimeRemoteFirstEntryStatus.RejectedAuthority;
+ }
+ // The withdrawal acknowledgement may have re-armed (or —
+ // when the generation was already ready — synchronously
+ // resubmitted) the parked operation; peek again.
+ continue;
+ }
+ if (head.Kind is RuntimePlacementProjectionKind.Place)
+ {
+ progress.Projection = head.Token;
+ progress.Stage = Stage.PlacementCommitted;
+ break;
+ }
+ // Discard (a delete/cancel rewrote our slot) or any other
+ // kind bearing our key: authority moved. Leave the receipt
+ // for the ordinary host drain — mirroring the C3a
+ // conductor's abandonment, which never consumes a Discard
+ // it did not publish — and fail closed.
+ Discard(key);
+ return RuntimeRemoteFirstEntryStatus.RejectedAuthority;
+ }
+ }
+
+ if (progress.Stage is Stage.PlacementCommitted)
+ {
+ if (!_physics.SetPosition.AcknowledgeProjection(progress.Projection))
+ {
+ // Same re-validation the C3a conductor performs on a failed
+ // acknowledge: only a genuinely-not-our-turn FIFO head stays
+ // retryable; a retired lease or a rewritten/superseded slot
+ // means authority moved.
+ if (!IsAcknowledgementStillPending(
+ record, residenceToken, progress.Projection))
+ {
+ Discard(key);
+ return RuntimeRemoteFirstEntryStatus.RejectedAuthority;
+ }
+ return RuntimeRemoteFirstEntryStatus
+ .AwaitingReceiptAcknowledgement;
+ }
+ progress.Stage = Stage.Acknowledged;
+ }
+
+ return RunExecute(
+ record,
+ residenceToken,
+ inputs,
+ key,
+ progress,
+ out receipt,
+ out construction);
+ }
+
+ ///
+ /// Re-validates authority after a failed acknowledge — the exact C3a
+ /// mechanism, shared verbatim with the local-player conductor via
+ /// (C3b
+ /// review M2: one body, so the abandonment fix cannot regress
+ /// independently in either conductor). Full rationale on the shared
+ /// helper.
+ ///
+ private bool IsAcknowledgementStillPending(
+ RuntimeEntityRecord record,
+ in RuntimeInitialCreateResidenceToken residenceToken,
+ in RuntimePlacementProjectionToken expected) =>
+ RuntimeFirstEntryAcknowledgement.IsStillPending(
+ _residences,
+ _physics.SetPosition,
+ record,
+ residenceToken,
+ expected);
+
+ private RuntimeRemoteFirstEntryStatus RunExecute(
+ RuntimeEntityRecord record,
+ in RuntimeInitialCreateResidenceToken residenceToken,
+ in RuntimeInitialCreateExecutionInputs inputs,
+ RuntimeEntityKey key,
+ Progress progress,
+ out RuntimeInitialCreateExecutionReceipt receipt,
+ out RuntimeRemoteBodyConstructionReceipt construction)
+ {
+ construction = default;
+ RuntimeInitialCreateExecutionStatus executeStatus = _executor.Execute(
+ record, residenceToken, inputs, out receipt);
+ switch (executeStatus)
+ {
+ case RuntimeInitialCreateExecutionStatus.Completed:
+ // C3b review M1: the terminal Advance is the one natural
+ // consumption point — deliver the construction receipt in
+ // the same call that reaps its only retained storage (this
+ // progress entry). Default (no body constructed) for a
+ // route that performs no SetPosition.
+ construction = progress.Construction;
+ _progress.Remove(key);
+ return RuntimeRemoteFirstEntryStatus.Completed;
+ case RuntimeInitialCreateExecutionStatus.PendingPlacement:
+ return RuntimeRemoteFirstEntryStatus
+ .AwaitingReceiptAcknowledgement;
+ case RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement:
+ return RuntimeRemoteFirstEntryStatus
+ .AwaitingContinuationPlacement;
+ case RuntimeInitialCreateExecutionStatus.RejectedToken:
+ _progress.Remove(key);
+ return RuntimeRemoteFirstEntryStatus.RejectedToken;
+ default:
+ _progress.Remove(key);
+ return RuntimeRemoteFirstEntryStatus.RejectedAuthority;
+ }
+ }
+
+ ///
+ /// Drops this class's own progress entry for .
+ /// Unlike the C3a conductor there is no publication candidate/activation
+ /// to discard — the constructed body, once bound through the canonical
+ /// writer, belongs to the record and is torn down by ordinary entity
+ /// teardown (retail has no entry-flow rollback; the C3a carried finding
+ /// applies identically here). The residence and executor own their own
+ /// convergence independently.
+ ///
+ private void Discard(RuntimeEntityKey key) => _progress.Remove(key);
+
+ ///
+ /// Cleanup for one key. binds
+ /// this into 's multicast
+ /// retirement notification (alongside the executor's
+ /// DiscardProgress and the C3a conductor's Forget), so any
+ /// residence retirement path — delete, reset, generation replacement, a
+ /// host discovering staleness — reaps this class's progress
+ /// automatically, using the exact key the residence tracked internally.
+ ///
+ internal void Forget(RuntimeEntityKey key) => Discard(key);
+
+ ///
+ /// Bulk cleanup wired into the same session-clear sequence
+ /// () as the
+ /// executor's and the C3a conductor's own DiscardAll calls.
+ ///
+ internal void DiscardAll() => _progress.Clear();
+
+ internal RuntimeRemoteFirstEntryOwnershipSnapshot CaptureOwnership() =>
+ new(_progress.Count);
+}
diff --git a/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerFirstEntryState.cs b/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerFirstEntryState.cs
index c5629e5d..f366c733 100644
--- a/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerFirstEntryState.cs
+++ b/src/AcDream.Runtime/Gameplay/RuntimeLocalPlayerFirstEntryState.cs
@@ -533,49 +533,26 @@ internal sealed class RuntimeLocalPlayerFirstEntryState
}
///
- /// Re-validates authority after a failed acknowledge. Two independent
- /// checks, either of which failing means authority moved and this class
- /// must abandon rather than keep retrying forever: (1) the residence
- /// lease this whole sequence began under must still be exactly current
- /// (mirrors the stage0/stage1 checks — a delete or reset retires it);
- /// (2) if the FIFO head belongs to THIS entity at all, it must still be
- /// the exact Place projection this class is holding — a head that
- /// belongs to us but is no longer that exact token (rewritten to
- /// Discard, or to a later revision) means our specific placement was
- /// superseded even if the residence lookup transiently still resolves.
- /// A head belonging to a DIFFERENT entity is the genuine "not yet our
- /// turn" case and must stay retryable. This reads the Runtime-internal
- /// directly
- /// rather than through the public, generation-gated
- /// — this class is part
- /// of Runtime, not an external host crossing that boundary, exactly like
- /// its existing direct
- /// call above.
+ /// Re-validates authority after a failed acknowledge. C3b review M2:
+ /// the mechanism (residence-lease currency + exact-head-token match; a
+ /// DIFFERENT entity's head stays retryable) is shared verbatim with the
+ /// remote conductor via
+ /// — one
+ /// body, so the abandonment fix that stops a delete-rewritten Discard
+ /// head from producing an infinite AwaitingReceiptAcknowledgement retry
+ /// cannot regress independently in either conductor. Full rationale on
+ /// the shared helper.
///
private bool IsAcknowledgementStillPending(
RuntimeEntityRecord record,
in RuntimeInitialCreateResidenceToken residenceToken,
- in RuntimePlacementProjectionToken expected)
- {
- if (!_residences.TryGetCurrent(
- record,
- out RuntimeInitialCreateResidenceLease lease)
- || lease.Token != residenceToken)
- {
- return false;
- }
-
- if (_physics.SetPosition.TryPeekProjection(
- out RuntimePlacementProjectionSnapshot head)
- && head.Token.Entity == expected.Entity
- && (head.Kind is not RuntimePlacementProjectionKind.Place
- || head.Token != expected))
- {
- return false;
- }
-
- return true;
- }
+ in RuntimePlacementProjectionToken expected) =>
+ RuntimeFirstEntryAcknowledgement.IsStillPending(
+ _residences,
+ _physics.SetPosition,
+ record,
+ residenceToken,
+ expected);
private RuntimeLocalPlayerFirstEntryStatus RunExecute(
RuntimeEntityRecord record,
diff --git a/tests/AcDream.Runtime.Tests/Entities/RuntimeRemoteFirstEntryStateTests.cs b/tests/AcDream.Runtime.Tests/Entities/RuntimeRemoteFirstEntryStateTests.cs
new file mode 100644
index 00000000..7f54988d
--- /dev/null
+++ b/tests/AcDream.Runtime.Tests/Entities/RuntimeRemoteFirstEntryStateTests.cs
@@ -0,0 +1,980 @@
+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(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(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.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(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(),
+ Array.Empty(),
+ 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(),
+ Array.Empty(),
+ 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.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(),
+ TextureChanges: Array.Empty(),
+ SubPalettes: Array.Empty(),
+ 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 ReadSetupCollision(
+ uint sourceFileId,
+ CancellationToken cancellationToken = default)
+ {
+ ReadCount++;
+ Assert.Equal(expectedSetupTableId, sourceFileId);
+ return Status switch
+ {
+ PreparedAssetReadStatus.Loaded =>
+ PreparedCollisionReadResult.Loaded(setup),
+ PreparedAssetReadStatus.Corrupt =>
+ PreparedCollisionReadResult.Corrupt,
+ _ => PreparedCollisionReadResult.Missing,
+ };
+ }
+
+ public PreparedCollisionReadResult
+ ReadGfxObjCollision(
+ uint sourceFileId,
+ CancellationToken cancellationToken = default) =>
+ throw new NotSupportedException(
+ "Only ReadSetupCollision is exercised by these tests.");
+
+ public PreparedCollisionReadResult
+ ReadCellStructureCollision(
+ uint sourceFileId,
+ CancellationToken cancellationToken = default) =>
+ throw new NotSupportedException(
+ "Only ReadSetupCollision is exercised by these tests.");
+
+ public PreparedCollisionReadResult
+ 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(),
+ Array.Empty(),
+ worldOffsetX: 0f,
+ worldOffsetY: 0f);
+ Lifetime = new RuntimeEntityObjectLifetime(engine);
+ }
+ else
+ {
+ Lifetime = new RuntimeEntityObjectLifetime();
+ }
+ var generation = new RuntimeGenerationToken(1UL);
+ Lifetime.BindEventContext(() => generation, static () => 1UL);
+
+ // 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.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();
+ }
+}