feat(runtime): construct remote bodies at Create per retail set_description

Cutover slice C3b: residence-route remote/creature/projectile Creates now
get their canonical PhysicsBody at Create time — retail's order, closing
the C3 flip's Finding C (production builds bodies at first motion; retail
builds them in ACCObjectMaint::CreateObject). RuntimeRemoteBodyDescription
walks set_description 0x00514F40 exactly: the motion-table gate (zero id
PASSES — verified at 0051871f/005127ca), the frame-vs-movement branch
keyed on retail's movement_buffer != 0 (an empty-buffer movement payload
takes the PLACEMENT branch and writes no autonomy — the wire-shape defect
the retail review caught), set_state, the byte-certain friction gate
(inclusive [0,1]; NaN deliberately skipped per the gates doc's sanctioned
deviation), the set_elasticity clamp with retail's unordered-to-zero NaN
routing (ACE diverges to 0.1 on that edge), the translucency gate
(!= 0.0f, original always recorded), velocity via setter, omega raw, and
ctor-defaults for absent wire fields (0.95f/0.05f/0 — the fresh-desc-per-
message flow verified at both UnPack call sites). InWorld stays false
until submission, the enter_world analog.

RuntimeRemoteFirstEntryState sequences mover-prep -> body construction ->
placement -> acknowledgement -> Execute with every C3a hardening
inherited: exactly-once stages, the shared acknowledge-stage
discriminator (extracted to RuntimeFirstEntryAcknowledgement, one body
for both conductors), typed Contention against in-flight remote-motion
binds, never-clobber body binding through the canonical writer (foreign
body fails closed — provably safe coexistence with today's
build-at-first-motion path in both directions), automatic convergence
through the retirement fan-out, and the construction receipt riding the
terminal Advance. Dormant: no production caller; C3c wires both hosts.

Reviewed: retail-conformance PASS (the construction order, both gate
boundary/NaN semantics, the motion-table and autonomy verdicts all
re-derived from the pseudo-C) + architecture/adversarial PASS after one
fix round. Runtime 982/982; complete Release solution 10,777 passed / 4
intentional skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-02 10:20:48 +02:00
parent d62b99509e
commit 0934a12111
6 changed files with 2083 additions and 41 deletions

View file

@ -64,7 +64,13 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
/// <c>Advance</c>), but fully constructed/wired like every other owner
/// here, so its own ownership must converge to zero the same way.
/// </summary>
int LocalPlayerFirstEntryActiveCount = 0)
int LocalPlayerFirstEntryActiveCount = 0,
/// <summary>
/// C3b: outstanding <see cref="RuntimeRemoteFirstEntryState"/> tracked
/// keys - the remote/projectile Create-time body-construction conductor.
/// Dormant like its C3a sibling; converges to zero the same way.
/// </summary>
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();

View file

@ -0,0 +1,66 @@
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Entities;
/// <summary>
/// The ONE post-failed-acknowledge re-validation both first-entry conductors
/// (<c>AcDream.Runtime.Gameplay.RuntimeLocalPlayerFirstEntryState</c> and
/// <see cref="RuntimeRemoteFirstEntryState"/>) 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
/// <c>AcknowledgeProjection</c> fail forever while the conductor retried
/// endlessly — cannot regress independently in either copy. Follows the
/// class family's pure-static-helper convention
/// (<see cref="RuntimeSetPositionMoverPreparer"/>,
/// <see cref="RuntimeInitialCreateAdmissionFreezer"/>,
/// <see cref="RuntimeAuthoritativePositionRouteClassifier"/>).
/// </summary>
internal static class RuntimeFirstEntryAcknowledgement
{
/// <summary>
/// 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
/// <see cref="RuntimeSetPositionState.TryPeekProjection"/> directly
/// rather than through the public, generation-gated
/// <see cref="RuntimePlacementProjectionChannel"/> — the conductors are
/// part of Runtime, not external hosts crossing that boundary, exactly
/// like their existing direct
/// <see cref="RuntimeSetPositionState.AcknowledgeProjection"/> calls.
/// </summary>
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;
}
}

View file

@ -0,0 +1,321 @@
using System.Numerics;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Entities;
/// <summary>
/// Typed record of one retail-ordered body construction. Fields with no
/// <see cref="PhysicsBody"/> 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.
/// </summary>
internal readonly record struct RuntimeRemoteBodyConstructionReceipt(
/// <summary>The motion-table id retail's gate evaluated (0 = none on the wire).</summary>
uint MotionTableId,
/// <summary>
/// Retail <c>CPhysicsObj::SetMotionTableID</c> (0x00512780,
/// pseudo-C:280528) fails ONLY when <c>part_array == 0</c> (005127da) or
/// when <c>MotionTableManager::Create</c> fails for a NONZERO id
/// (<c>CPartArray::SetMotionTableID</c> 0x005186E0, pseudo-C:286732,
/// 0051872f). A ZERO id skips manager creation (0051871f falls through
/// to <c>return 1</c> at 00518743) and <c>CPhysicsObj</c> then skips
/// <c>MakeMovementManager</c> 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.
/// </summary>
bool MotionTableGatePassed,
/// <summary>
/// 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.
/// </summary>
bool MovementBranch,
/// <summary>Retail <c>last_move_was_autonomous</c> written on the movement branch.</summary>
bool LastMoveWasAutonomous,
/// <summary>True when the no-movement branch staged the dormant cell frame.</summary>
bool PlacementFrameStaged,
/// <summary>The friction value the body carries after the gated write.</summary>
float Friction,
/// <summary>
/// Friction gate outcome — byte-certain gates 1+2 of
/// docs/research/2026-08-02-set-description-float-gates.md: applied only
/// when <c>0.0f &lt;= friction &lt;= 1.0f</c>.
/// </summary>
bool FrictionApplied,
/// <summary>The elasticity the body carries after retail set_elasticity's clamp.</summary>
float Elasticity,
/// <summary>
/// Retail's UNCONDITIONAL <c>translucencyOriginal</c> write
/// (0x00515097, gates doc conditional 3 — written before the gate,
/// always). <see cref="PhysicsBody"/> 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.
/// </summary>
float TranslucencyOriginal,
/// <summary>
/// Translucency gate outcome — byte-certain gate 3: live translucency
/// (+ PartArray propagation, presentation-side) only when
/// <c>translucency != 0.0f</c>.
/// </summary>
bool TranslucencyApplied,
/// <summary>True when a present, finite wire velocity was applied via set_velocity.</summary>
bool VelocityApplied,
/// <summary>True when a present, finite wire omega was written (raw field, no setter).</summary>
bool OmegaApplied);
/// <summary>
/// Pure retail-ordered construction of one remote/projectile canonical
/// <see cref="PhysicsBody"/> from the frozen wire <see cref="PhysicsSpawnData"/>,
/// per <c>CPhysicsObj::set_description</c> (0x00514F40, pseudo-C:283155-283276)
/// as called from <c>ACCObjectMaint::CreateObject</c> (0x00558870,
/// pseudo-C:356155-356245, step 6). The caller
/// (<see cref="RuntimeRemoteFirstEntryState"/>) sequences this AFTER
/// mover preparation (retail: <c>CPhysicsObj::makeObject</c> shapes the
/// Setup/part-array before set_description runs) and binds the result through
/// the canonical <see cref="RuntimePhysicsState.GetOrCreatePhysicsBody"/>
/// writer — never a parallel binding idiom.
///
/// Field routing, per the retail order:
/// <list type="number">
/// <item>Motion-table gate — evaluated (see
/// <see cref="RuntimeRemoteBodyConstructionReceipt.MotionTableGatePassed"/>);
/// motion-table DAT installation stays presentation-side.</item>
/// <item>Sound table (step 2) / physics-script table (step 3) — snapshot/
/// presentation-side today; not body fields.</item>
/// <item>Placement-frame-vs-Movement (step 4, mutually exclusive) — movement
/// payload present writes <see cref="PhysicsBody.LastMoveWasAutonomous"/>
/// (retail <c>last_move_was_autonomous = get_autonomous_movement</c>) 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
/// <c>SetPlacementFrameInternal</c>; enter_world remains the later
/// submission — <see cref="PhysicsBody.InWorld"/> stays false).</item>
/// <item>set_state (step 5) — <see cref="PhysicsBody.State"/> from
/// <see cref="RuntimeEntityRecord.FinalPhysicsState"/>, the retail
/// state-transition view of the wire state (mirrors the existing canonical
/// writers' convention: <c>RuntimePhysicsState.InitializeNewPhysicsBody</c>,
/// <c>SetRemoteMotion</c>).</item>
/// <item>Scale (step 6) — NOT a <see cref="PhysicsBody"/> field; it rides
/// the prepared mover command
/// (<see cref="RuntimeSetPositionMoverPreparer.TryBuild"/> reads
/// <c>Snapshot.Physics.Scale ?? ObjScale ?? 1f</c>, matching the
/// PhysicsDesc constructor default 1f at 0x0051D4D0).</item>
/// <item>Friction (step 7) — gated per the byte-certain gates doc.</item>
/// <item>Elasticity (step 8) — via the set_elasticity clamp port below.</item>
/// <item>Translucency (step 9) — original always recorded; live apply gated;
/// both receipt-side (no body slot; presentation owns render alpha).</item>
/// <item>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 <c>InitializeNewPhysicsBody</c>
/// convention, mirrored here — produces the identical end state for an
/// absent field (the fresh body's velocity/omega are already zero).</item>
/// <item>default_script / default_script_intensity (step 12) — snapshot/
/// presentation-side; not body fields.</item>
/// <item>All nine PhysicsTimeStamp slots (step 13, LAST) — already applied
/// at admission and frozen on the residence lease
/// (<see cref="PhysicsSpawnData.Timestamps"/>); nothing body-side.</item>
/// </list>
/// </summary>
internal static class RuntimeRemoteBodyDescription
{
/// <summary>
/// PhysicsDesc constructor default friction: 0x0051D4D0 (pseudo-C:292056)
/// writes bytes <c>"33s?"</c> = 0x3F733333 = 0.95f. Same value as
/// <see cref="PhysicsBody.DefaultFriction"/>.
/// </summary>
private const float DefaultDescFriction = 0.95f;
/// <summary>PhysicsDesc constructor default elasticity (0x0051D4D0): 0.05f.</summary>
private const float DefaultDescElasticity = 0.05f;
/// <summary>
/// Retail <c>CPhysicsObj::set_elasticity</c> (0x0050FD40,
/// pseudo-C:277817) upper clamp constant 0.100000001f (float 0.1).
/// Cross-check: ACE <c>PhysicsGlobals.MaxElasticity = 0.1f</c>
/// (ACE PhysicsObj.cs:3586-3599 reproduces the identical clamp).
/// </summary>
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);
}

View file

@ -0,0 +1,649 @@
using AcDream.Content;
using AcDream.Core.Physics;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Entities;
/// <summary>
/// Typed yields for <see cref="RuntimeRemoteFirstEntryState.Advance"/>.
/// Mirrors the C3a conductor's vocabulary
/// (<c>RuntimeLocalPlayerFirstEntryStatus</c>) rather than inventing a
/// parallel one; the two publication-only statuses have no remote analog.
/// </summary>
internal enum RuntimeRemoteFirstEntryStatus : byte
{
/// <summary>
/// The underlying <see cref="RuntimeInitialCreateContinuationExecutor.Execute"/>
/// call reported <c>Completed</c>: residence consumed, initial tail and
/// FIFO drained, ExecutorCompleted receipt dispatched. Terminal.
/// </summary>
Completed,
/// <summary>
/// The authored-mover Setup read
/// (<see cref="RuntimeSetPositionState.TryPrepareAuthoredMover"/>) is not
/// yet available. Retry with the same arguments once the prepared-asset
/// package lands; no Runtime state changed.
/// </summary>
AwaitingCollisionSource,
/// <summary>
/// The submitted placement deferred (<c>DeferredCell</c> — 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
/// <see cref="RuntimeSetPositionState"/> (collision-generation commit
/// drives <c>RetryDeferred</c>); retry <see cref="Advance"/> after it.
/// </summary>
AwaitingPlacement,
/// <summary>
/// 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 <see cref="RuntimeInitialCreateContinuationExecutor.Execute"/>
/// still observed <c>PendingPlacement</c>. Retry the same stage.
/// </summary>
AwaitingReceiptAcknowledgement,
/// <summary>
/// Passthrough of the executor's own <c>AwaitingContinuationPlacement</c>
/// — a later FIFO continuation needs its own authored placement before
/// the drain can finish; entirely the executor's concern from here on.
/// </summary>
AwaitingContinuationPlacement,
/// <summary>
/// A reentrant <see cref="Advance"/> 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.
/// </summary>
Contention,
/// <summary>
/// The residence token matches nothing this conductor can own — including
/// a LOCAL-PLAYER lease (<see cref="RuntimeSetPositionOperationKind.InitialLogin"/>),
/// which belongs to the C3a conductor, never this one.
/// </summary>
RejectedToken,
/// <summary>
/// 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.
/// </summary>
RejectedAuthority,
}
internal readonly record struct RuntimeRemoteFirstEntryOwnershipSnapshot(
int ActiveCount)
{
internal bool IsConverged => ActiveCount == 0;
}
/// <summary>
/// The dormant, resumable Runtime transaction that dissolves C3's Finding C:
/// ordinary remote-creature and projectile Creates classify to
/// <c>SetPosition</c>, but no production path constructs their canonical
/// <see cref="PhysicsBody"/> at Create time (bodies arrive with first motion
/// today), so <see cref="RuntimeSetPositionState.SubmitPreparedPlacement"/>'s
/// <c>Record.PhysicsBody</c> requirement rejects the residence route's
/// initial placement. This class is the remote analog of the C3a conductor
/// (<c>RuntimeLocalPlayerFirstEntryState</c>) WITHOUT the publication chain —
/// remotes have no <c>PlayerMovementController</c> — and with retail body
/// construction in its place:
///
/// mover preparation (retail <c>CPhysicsObj::makeObject</c> shaping the
/// Setup, which precedes <c>set_description</c> in
/// <c>ACCObjectMaint::CreateObject</c> 0x00558870 step 2-vs-6) -&gt;
/// body construction per the exact <c>set_description</c> order
/// (0x00514F40; <see cref="RuntimeRemoteBodyDescription"/>) bound through the
/// canonical <see cref="RuntimePhysicsState.GetOrCreatePhysicsBody"/> writer
/// -&gt; ordinary authored submission
/// (<see cref="RuntimeSetPositionState.SubmitPreparedPlacement"/> — retail
/// <c>enter_world</c>, which <c>SmartBox::HandleCreateObject</c> 0x00454C80
/// runs for a top-level object with a nonzero wire cell AFTER CreateObject
/// returns) -&gt; Withdraw/Place receipt acknowledgement -&gt;
/// <see cref="RuntimeInitialCreateContinuationExecutor.Execute"/> (FIFO
/// drain).
///
/// Unlike the local-player path this class never touches the dormant
/// activation family: no operation it drives ever has
/// <c>DormantLocalActivation</c> set, so the ordinary submission tail is the
/// correct — and only — commit route.
///
/// Dormant by design: <see cref="RuntimeEntityObjectLifetime"/> fully
/// constructs and wires this class (construction, retirement fan-out, bulk
/// session-clear cleanup, ownership fold) exactly like the C3a conductor,
/// but nothing calls <see cref="Advance"/> in production — C3c wires the
/// hosts.
/// </summary>
internal sealed class RuntimeRemoteFirstEntryState
{
private enum Stage : byte
{
/// <summary>No progress yet, or the mover has not been prepared.</summary>
AwaitingMoverPreparation,
/// <summary>Mover command in hand; the body has not been constructed.</summary>
MoverPrepared,
/// <summary>
/// The canonical body is constructed and bound; the placement has
/// not been submitted.
/// </summary>
BodyConstructed,
/// <summary>
/// Submission deferred (<c>DeferredCell</c>): the parked operation's
/// Withdraw/Place receipts are drained from the projection FIFO as
/// they surface; the wake itself is internal to
/// <see cref="RuntimeSetPositionState"/>.
/// </summary>
PlacementSubmitted,
/// <summary>
/// The Place projection token is known but not yet acknowledged.
/// </summary>
PlacementCommitted,
/// <summary>
/// The Place projection has been acknowledged. Only
/// <see cref="RuntimeInitialCreateContinuationExecutor.Execute"/>
/// remains; the acknowledgement step is never re-entered.
/// </summary>
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<RuntimeEntityKey, Progress> _progress = [];
private readonly HashSet<RuntimeEntityKey> _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));
}
/// <summary>
/// Exposes the body-construction receipt for a still-tracked entry —
/// the MID-FLIGHT half of the consumption rule documented on
/// <see cref="Advance"/> (C3b review M1): while the sequence is in
/// flight this query serves diagnostics/tests; the terminal
/// <c>Completed</c> 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.
/// </summary>
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;
}
/// <summary>
/// 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.
///
/// <para><b>Construction-receipt consumption rule (C3b review M1),
/// following the C3a/F2 precedent of receipts riding the terminal
/// Advance out-params:</b> <paramref name="construction"/> is populated
/// ONLY on the <see cref="RuntimeRemoteFirstEntryStatus.Completed"/>
/// yield — the same call that delivers the executor
/// <paramref name="receipt"/> — 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
/// <see cref="TryGetConstruction"/>; after Completed nothing is
/// retained. A lease whose route performs no SetPosition (Parented/
/// PickedUp) constructs no body, so its terminal receipt is default.</para>
/// </summary>
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);
}
/// <summary>
/// Re-validates authority after a failed acknowledge — the exact C3a
/// mechanism, shared verbatim with the local-player conductor via
/// <see cref="RuntimeFirstEntryAcknowledgement.IsStillPending"/> (C3b
/// review M2: one body, so the abandonment fix cannot regress
/// independently in either conductor). Full rationale on the shared
/// helper.
/// </summary>
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;
}
}
/// <summary>
/// Drops this class's own progress entry for <paramref name="key"/>.
/// 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.
/// </summary>
private void Discard(RuntimeEntityKey key) => _progress.Remove(key);
/// <summary>
/// Cleanup for one key. <see cref="RuntimeEntityObjectLifetime"/> binds
/// this into <see cref="RuntimeInitialCreateResidenceState"/>'s multicast
/// retirement notification (alongside the executor's
/// <c>DiscardProgress</c> and the C3a conductor's <c>Forget</c>), 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.
/// </summary>
internal void Forget(RuntimeEntityKey key) => Discard(key);
/// <summary>
/// Bulk cleanup wired into the same session-clear sequence
/// (<see cref="RuntimeEntityObjectLifetime.BeginSessionClear"/>) as the
/// executor's and the C3a conductor's own <c>DiscardAll</c> calls.
/// </summary>
internal void DiscardAll() => _progress.Clear();
internal RuntimeRemoteFirstEntryOwnershipSnapshot CaptureOwnership() =>
new(_progress.Count);
}

View file

@ -533,49 +533,26 @@ internal sealed class RuntimeLocalPlayerFirstEntryState
}
/// <summary>
/// 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
/// <see cref="RuntimeSetPositionState.TryPeekProjection"/> directly
/// rather than through the public, generation-gated
/// <see cref="RuntimePlacementProjectionChannel"/> — this class is part
/// of Runtime, not an external host crossing that boundary, exactly like
/// its existing direct <see cref="RuntimeSetPositionState.AcknowledgeProjection"/>
/// 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
/// <see cref="RuntimeFirstEntryAcknowledgement.IsStillPending"/> — 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.
/// </summary>
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,