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:
parent
d62b99509e
commit
0934a12111
6 changed files with 2083 additions and 41 deletions
|
|
@ -64,7 +64,13 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
||||||
/// <c>Advance</c>), but fully constructed/wired like every other owner
|
/// <c>Advance</c>), but fully constructed/wired like every other owner
|
||||||
/// here, so its own ownership must converge to zero the same way.
|
/// here, so its own ownership must converge to zero the same way.
|
||||||
/// </summary>
|
/// </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 =>
|
public bool IsConverged =>
|
||||||
IsDisposed
|
IsDisposed
|
||||||
|
|
@ -87,6 +93,7 @@ public readonly record struct RuntimeEntityObjectOwnershipSnapshot(
|
||||||
&& InitialCreateExecutorProgressCount == 0
|
&& InitialCreateExecutorProgressCount == 0
|
||||||
&& PendingCompletionReceiptCount == 0
|
&& PendingCompletionReceiptCount == 0
|
||||||
&& LocalPlayerFirstEntryActiveCount == 0
|
&& LocalPlayerFirstEntryActiveCount == 0
|
||||||
|
&& RemoteFirstEntryActiveCount == 0
|
||||||
&& StreamSubscriberCount == 0
|
&& StreamSubscriberCount == 0
|
||||||
&& PlacementStreamSubscriberCount == 0
|
&& PlacementStreamSubscriberCount == 0
|
||||||
&& PendingDispatchCount == 0
|
&& PendingDispatchCount == 0
|
||||||
|
|
@ -176,6 +183,15 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
InitialCreateResidences,
|
InitialCreateResidences,
|
||||||
InitialCreateExecution,
|
InitialCreateExecution,
|
||||||
Physics);
|
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
|
// Round 3 B3: every residence retirement path - not only the
|
||||||
// executor's own DiscardProgress calls - must converge the
|
// executor's own DiscardProgress calls - must converge the
|
||||||
// executor's progress AND its separately-tracked pending
|
// 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.
|
// updated doc comment for why this is now multicast.
|
||||||
InitialCreateResidences.BindRetirementNotification(
|
InitialCreateResidences.BindRetirementNotification(
|
||||||
key => LocalPlayerFirstEntry.Forget(key));
|
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
|
// F2: reaps the executor's completion-receipt correlation entry
|
||||||
// exactly when a host acknowledges the ExecutorCompleted receipt it
|
// exactly when a host acknowledges the ExecutorCompleted receipt it
|
||||||
// correlates - mirrors the residence-retirement binding immediately
|
// correlates - mirrors the residence-retirement binding immediately
|
||||||
|
|
@ -245,6 +265,15 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
InitialCreateResidences,
|
InitialCreateResidences,
|
||||||
InitialCreateExecution,
|
InitialCreateExecution,
|
||||||
Physics);
|
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
|
// Round 3 B3: every residence retirement path - not only the
|
||||||
// executor's own DiscardProgress calls - must converge the
|
// executor's own DiscardProgress calls - must converge the
|
||||||
// executor's progress AND its separately-tracked pending
|
// 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.
|
// updated doc comment for why this is now multicast.
|
||||||
InitialCreateResidences.BindRetirementNotification(
|
InitialCreateResidences.BindRetirementNotification(
|
||||||
key => LocalPlayerFirstEntry.Forget(key));
|
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
|
// F2: reaps the executor's completion-receipt correlation entry
|
||||||
// exactly when a host acknowledges the ExecutorCompleted receipt it
|
// exactly when a host acknowledges the ExecutorCompleted receipt it
|
||||||
// correlates - mirrors the residence-retirement binding immediately
|
// correlates - mirrors the residence-retirement binding immediately
|
||||||
|
|
@ -314,6 +347,15 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
InitialCreateResidences,
|
InitialCreateResidences,
|
||||||
InitialCreateExecution,
|
InitialCreateExecution,
|
||||||
Physics);
|
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
|
// Round 3 B3: every residence retirement path - not only the
|
||||||
// executor's own DiscardProgress calls - must converge the
|
// executor's own DiscardProgress calls - must converge the
|
||||||
// executor's progress AND its separately-tracked pending
|
// 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.
|
// updated doc comment for why this is now multicast.
|
||||||
InitialCreateResidences.BindRetirementNotification(
|
InitialCreateResidences.BindRetirementNotification(
|
||||||
key => LocalPlayerFirstEntry.Forget(key));
|
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
|
// F2: reaps the executor's completion-receipt correlation entry
|
||||||
// exactly when a host acknowledges the ExecutorCompleted receipt it
|
// exactly when a host acknowledges the ExecutorCompleted receipt it
|
||||||
// correlates - mirrors the residence-retirement binding immediately
|
// correlates - mirrors the residence-retirement binding immediately
|
||||||
|
|
@ -353,6 +399,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
internal RuntimeInitialCreateContinuationExecutor InitialCreateExecution
|
internal RuntimeInitialCreateContinuationExecutor InitialCreateExecution
|
||||||
{ get; }
|
{ get; }
|
||||||
internal RuntimeLocalPlayerFirstEntryState LocalPlayerFirstEntry { get; }
|
internal RuntimeLocalPlayerFirstEntryState LocalPlayerFirstEntry { get; }
|
||||||
|
internal RuntimeRemoteFirstEntryState RemoteFirstEntry { get; }
|
||||||
|
|
||||||
public RuntimeEntityObjectOwnershipSnapshot CaptureOwnership()
|
public RuntimeEntityObjectOwnershipSnapshot CaptureOwnership()
|
||||||
{
|
{
|
||||||
|
|
@ -389,7 +436,8 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
InitialCreateExecution.ReplayFailureCount,
|
InitialCreateExecution.ReplayFailureCount,
|
||||||
InitialCreateExecution.LastReplayFailure is not null,
|
InitialCreateExecution.LastReplayFailure is not null,
|
||||||
InitialCreateExecution.PendingCompletionReceiptCount,
|
InitialCreateExecution.PendingCompletionReceiptCount,
|
||||||
LocalPlayerFirstEntry.CaptureOwnership().ActiveCount);
|
LocalPlayerFirstEntry.CaptureOwnership().ActiveCount,
|
||||||
|
RemoteFirstEntry.CaptureOwnership().ActiveCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BindEventContext(
|
public void BindEventContext(
|
||||||
|
|
@ -1734,6 +1782,7 @@ public sealed class RuntimeEntityObjectLifetime : IDisposable
|
||||||
InitialCreateResidences.Clear();
|
InitialCreateResidences.Clear();
|
||||||
InitialCreateExecution.DiscardAll();
|
InitialCreateExecution.DiscardAll();
|
||||||
LocalPlayerFirstEntry.DiscardAll();
|
LocalPlayerFirstEntry.DiscardAll();
|
||||||
|
RemoteFirstEntry.DiscardAll();
|
||||||
Physics.CollisionReports.LeaveWorldBatch(active);
|
Physics.CollisionReports.LeaveWorldBatch(active);
|
||||||
Physics.ResetSessionPhysics();
|
Physics.ResetSessionPhysics();
|
||||||
Entities.BeginSessionClear();
|
Entities.BeginSessionClear();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
321
src/AcDream.Runtime/Entities/RuntimeRemoteBodyDescription.cs
Normal file
321
src/AcDream.Runtime/Entities/RuntimeRemoteBodyDescription.cs
Normal 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 <= friction <= 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);
|
||||||
|
}
|
||||||
649
src/AcDream.Runtime/Entities/RuntimeRemoteFirstEntryState.cs
Normal file
649
src/AcDream.Runtime/Entities/RuntimeRemoteFirstEntryState.cs
Normal 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) ->
|
||||||
|
/// body construction per the exact <c>set_description</c> order
|
||||||
|
/// (0x00514F40; <see cref="RuntimeRemoteBodyDescription"/>) bound through the
|
||||||
|
/// canonical <see cref="RuntimePhysicsState.GetOrCreatePhysicsBody"/> writer
|
||||||
|
/// -> 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) -> Withdraw/Place receipt acknowledgement ->
|
||||||
|
/// <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);
|
||||||
|
}
|
||||||
|
|
@ -533,49 +533,26 @@ internal sealed class RuntimeLocalPlayerFirstEntryState
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Re-validates authority after a failed acknowledge. Two independent
|
/// Re-validates authority after a failed acknowledge. C3b review M2:
|
||||||
/// checks, either of which failing means authority moved and this class
|
/// the mechanism (residence-lease currency + exact-head-token match; a
|
||||||
/// must abandon rather than keep retrying forever: (1) the residence
|
/// DIFFERENT entity's head stays retryable) is shared verbatim with the
|
||||||
/// lease this whole sequence began under must still be exactly current
|
/// remote conductor via
|
||||||
/// (mirrors the stage0/stage1 checks — a delete or reset retires it);
|
/// <see cref="RuntimeFirstEntryAcknowledgement.IsStillPending"/> — one
|
||||||
/// (2) if the FIFO head belongs to THIS entity at all, it must still be
|
/// body, so the abandonment fix that stops a delete-rewritten Discard
|
||||||
/// the exact Place projection this class is holding — a head that
|
/// head from producing an infinite AwaitingReceiptAcknowledgement retry
|
||||||
/// belongs to us but is no longer that exact token (rewritten to
|
/// cannot regress independently in either conductor. Full rationale on
|
||||||
/// Discard, or to a later revision) means our specific placement was
|
/// the shared helper.
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private bool IsAcknowledgementStillPending(
|
private bool IsAcknowledgementStillPending(
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
in RuntimeInitialCreateResidenceToken residenceToken,
|
in RuntimeInitialCreateResidenceToken residenceToken,
|
||||||
in RuntimePlacementProjectionToken expected)
|
in RuntimePlacementProjectionToken expected) =>
|
||||||
{
|
RuntimeFirstEntryAcknowledgement.IsStillPending(
|
||||||
if (!_residences.TryGetCurrent(
|
_residences,
|
||||||
record,
|
_physics.SetPosition,
|
||||||
out RuntimeInitialCreateResidenceLease lease)
|
record,
|
||||||
|| lease.Token != residenceToken)
|
residenceToken,
|
||||||
{
|
expected);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RuntimeLocalPlayerFirstEntryStatus RunExecute(
|
private RuntimeLocalPlayerFirstEntryStatus RunExecute(
|
||||||
RuntimeEntityRecord record,
|
RuntimeEntityRecord record,
|
||||||
|
|
|
||||||
|
|
@ -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<PhysicsBody>(fixture.Record.PhysicsBody);
|
||||||
|
Assert.True(body.InWorld);
|
||||||
|
Assert.Equal(Cell, fixture.Record.FullCellId);
|
||||||
|
// set_description field application from the frozen wire desc:
|
||||||
|
// friction 0.5 passed gates 1+2; elasticity 0.05 inside the
|
||||||
|
// set_elasticity clamp; state is the record's retail-transitioned
|
||||||
|
// view; omega was the raw step-11 write.
|
||||||
|
Assert.Equal(0.5f, body.Friction);
|
||||||
|
Assert.Equal(0.05f, body.Elasticity);
|
||||||
|
Assert.Equal(fixture.Record.FinalPhysicsState, body.State);
|
||||||
|
Assert.Equal(new Vector3(0f, 0f, 0.25f), body.Omega);
|
||||||
|
Assert.True(fixture.Lifetime.Physics.IsSpatialRoot(fixture.Record));
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
Assert.False(fixture.Conductor.TryGetConstruction(
|
||||||
|
fixture.Key, out _));
|
||||||
|
Assert.False(fixture.Lifetime.TryGetInitialCreateResidence(
|
||||||
|
fixture.Record, out _));
|
||||||
|
Assert.Equal(0, fixture.Lifetime.CaptureOwnership()
|
||||||
|
.InitialCreateResidenceLeaseCount);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.CaptureOwnership()
|
||||||
|
.InitialCreateExecutorProgressCount);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.CaptureOwnership()
|
||||||
|
.RemoteFirstEntryActiveCount);
|
||||||
|
|
||||||
|
// Retrying with the now-stale residence token is a distinct, safe
|
||||||
|
// no-op — nothing left to resume.
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeRemoteFirstEntryStatus.RejectedToken,
|
||||||
|
fixture.Advance(out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProjectileFlavorFullSequenceCompletesWithMissileState()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(
|
||||||
|
residentWorld: true,
|
||||||
|
rawState: (uint)(PhysicsStateFlags.Gravity
|
||||||
|
| PhysicsStateFlags.ReportCollisions
|
||||||
|
| PhysicsStateFlags.Missile
|
||||||
|
| PhysicsStateFlags.Inelastic
|
||||||
|
| PhysicsStateFlags.PathClipped
|
||||||
|
| PhysicsStateFlags.AlignPath));
|
||||||
|
|
||||||
|
// The Missile flag classified the Create to ProjectileAuthoritative
|
||||||
|
// (RuntimeInitialCreateResidenceState.Begin) — the conductor accepts
|
||||||
|
// that flavor through the identical stages.
|
||||||
|
Assert.True(fixture.Lifetime.TryGetInitialCreateResidence(
|
||||||
|
fixture.Record, out RuntimeInitialCreateResidenceLease lease));
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeSetPositionOperationKind.ProjectileAuthoritative,
|
||||||
|
lease.Route.OperationKind);
|
||||||
|
|
||||||
|
RuntimeRemoteFirstEntryStatus status = fixture.Advance(
|
||||||
|
out RuntimeInitialCreateExecutionReceipt receipt);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed, status);
|
||||||
|
Assert.Equal(Cell, receipt.FullCellId);
|
||||||
|
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
|
||||||
|
Assert.True(body.InWorld);
|
||||||
|
Assert.True(body.State.HasFlag(PhysicsStateFlags.Missile));
|
||||||
|
Assert.Equal(fixture.Record.FinalPhysicsState, body.State);
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// The three float gates, proven both ways on the pure construction
|
||||||
|
// (docs/research/2026-08-02-set-description-float-gates.md is law)
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(-1f, false, 0.95f)]
|
||||||
|
[InlineData(0f, true, 0f)]
|
||||||
|
[InlineData(0.5f, true, 0.5f)]
|
||||||
|
[InlineData(1f, true, 1f)]
|
||||||
|
[InlineData(1.5f, false, 0.95f)]
|
||||||
|
// R2b: NaN is SKIPPED (body keeps its default) — the gates doc's
|
||||||
|
// sanctioned deviation from retail's unordered-goes-to-apply codegen
|
||||||
|
// quirk; see the friction comment in RuntimeRemoteBodyDescription.
|
||||||
|
[InlineData(float.NaN, false, 0.95f)]
|
||||||
|
public void FrictionGateAppliesOnlyInsideClosedUnitInterval(
|
||||||
|
float wireFriction,
|
||||||
|
bool expectedApplied,
|
||||||
|
float expectedBodyFriction)
|
||||||
|
{
|
||||||
|
(PhysicsBody body, RuntimeRemoteBodyConstructionReceipt receipt) =
|
||||||
|
ConstructDirect(friction: wireFriction);
|
||||||
|
|
||||||
|
Assert.Equal(expectedApplied, receipt.FrictionApplied);
|
||||||
|
Assert.Equal(expectedBodyFriction, body.Friction);
|
||||||
|
Assert.Equal(body.Friction, receipt.Friction);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FrictionAbsentOnWireTakesTheDescConstructorDefault()
|
||||||
|
{
|
||||||
|
// PhysicsDesc ctor 0x0051D4D0 seeds friction 0.95f; it passes the
|
||||||
|
// gate, so an absent wire friction still records an APPLIED write.
|
||||||
|
(PhysicsBody body, RuntimeRemoteBodyConstructionReceipt receipt) =
|
||||||
|
ConstructDirect(friction: null);
|
||||||
|
|
||||||
|
Assert.True(receipt.FrictionApplied);
|
||||||
|
Assert.Equal(0.95f, body.Friction);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0f, false)]
|
||||||
|
[InlineData(0.5f, true)]
|
||||||
|
[InlineData(-0.25f, true)]
|
||||||
|
// R2: NaN lands in the apply bucket — matching retail's unordered case
|
||||||
|
// exactly (gates doc conditional 3: JNP fires ONLY on exact equality
|
||||||
|
// with 0.0f).
|
||||||
|
[InlineData(float.NaN, true)]
|
||||||
|
public void TranslucencyGateAppliesOnlyWhenNonZeroAndOriginalIsAlwaysRecorded(
|
||||||
|
float wireTranslucency,
|
||||||
|
bool expectedApplied)
|
||||||
|
{
|
||||||
|
(_, RuntimeRemoteBodyConstructionReceipt receipt) =
|
||||||
|
ConstructDirect(translucency: wireTranslucency);
|
||||||
|
|
||||||
|
// translucencyOriginal is written UNCONDITIONALLY (0x00515097,
|
||||||
|
// before the gate); the live apply fires for every value except
|
||||||
|
// exact 0.0f (gate 3's JNP fires only on equality).
|
||||||
|
Assert.Equal(wireTranslucency, receipt.TranslucencyOriginal);
|
||||||
|
Assert.Equal(expectedApplied, receipt.TranslucencyApplied);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TranslucencyAbsentOnWireIsZeroOriginalAndNoLiveApply()
|
||||||
|
{
|
||||||
|
(_, RuntimeRemoteBodyConstructionReceipt receipt) =
|
||||||
|
ConstructDirect(translucency: null);
|
||||||
|
|
||||||
|
Assert.Equal(0f, receipt.TranslucencyOriginal);
|
||||||
|
Assert.False(receipt.TranslucencyApplied);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(-0.5f, 0f)]
|
||||||
|
[InlineData(0f, 0f)]
|
||||||
|
[InlineData(0.05f, 0.05f)]
|
||||||
|
[InlineData(0.1f, 0.1f)]
|
||||||
|
[InlineData(0.2f, 0.1f)]
|
||||||
|
// R2a: retail's FIRST x87 compare sends unordered to the zeroing arm
|
||||||
|
// (0x0050FD51) — NaN -> 0f, NOT ACE's divergent NaN -> 0.1f.
|
||||||
|
[InlineData(float.NaN, 0f)]
|
||||||
|
public void ElasticityClampMatchesRetailSetter(
|
||||||
|
float wireElasticity,
|
||||||
|
float expectedBodyElasticity)
|
||||||
|
{
|
||||||
|
// CPhysicsObj::set_elasticity 0x0050FD40: < 0 -> 0; <= 0.1 -> value;
|
||||||
|
// > 0.1 -> 0.1 (ACE PhysicsGlobals.MaxElasticity = 0.1f agrees).
|
||||||
|
(PhysicsBody body, RuntimeRemoteBodyConstructionReceipt receipt) =
|
||||||
|
ConstructDirect(elasticity: wireElasticity);
|
||||||
|
|
||||||
|
Assert.Equal(expectedBodyElasticity, body.Elasticity);
|
||||||
|
Assert.Equal(body.Elasticity, receipt.Elasticity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Placement-frame-vs-movement branch (set_description step 4)
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MovementPayloadSuppressesPlacementFrameAndWritesAutonomy()
|
||||||
|
{
|
||||||
|
(PhysicsBody body, RuntimeRemoteBodyConstructionReceipt receipt) =
|
||||||
|
ConstructDirect(movement: new PhysicsMovementData(
|
||||||
|
RawData: new byte[] { 0x01 },
|
||||||
|
MotionState: null,
|
||||||
|
IsAutonomous: true));
|
||||||
|
|
||||||
|
Assert.True(receipt.MovementBranch);
|
||||||
|
Assert.True(receipt.LastMoveWasAutonomous);
|
||||||
|
Assert.True(body.LastMoveWasAutonomous);
|
||||||
|
Assert.False(receipt.PlacementFrameStaged);
|
||||||
|
// No frame staged: the body's cell identity stays default-detached.
|
||||||
|
Assert.Equal(0u, body.CellPosition.ObjCellId);
|
||||||
|
Assert.False(body.InWorld);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MovementFlagWithEmptyBufferTakesThePlacementBranch()
|
||||||
|
{
|
||||||
|
// R1: retail's branch discriminator is `movement_buffer != 0` —
|
||||||
|
// PhysicsDesc::UnPack (0x0051DDD0) assigns the buffer only inside
|
||||||
|
// `if (buff_length != 0)` (0051DE1F-0051DE2A), so a movement flag
|
||||||
|
// with a ZERO-LENGTH buffer reaches set_description with a null
|
||||||
|
// buffer and takes the PLACEMENT branch; the autonomy write (in the
|
||||||
|
// movement else-branch only) never runs. Our parser materializes
|
||||||
|
// exactly this wrapper shape (CreateObject.cs: non-null
|
||||||
|
// PhysicsMovementData with empty RawData). IsAutonomous is set true
|
||||||
|
// here deliberately — the BRANCH, never the wrapper's value, must
|
||||||
|
// decide.
|
||||||
|
(PhysicsBody body, RuntimeRemoteBodyConstructionReceipt receipt) =
|
||||||
|
ConstructDirect(movement: new PhysicsMovementData(
|
||||||
|
RawData: ReadOnlyMemory<byte>.Empty,
|
||||||
|
MotionState: null,
|
||||||
|
IsAutonomous: true));
|
||||||
|
|
||||||
|
Assert.False(receipt.MovementBranch);
|
||||||
|
Assert.True(receipt.PlacementFrameStaged);
|
||||||
|
Assert.Equal(Cell, body.CellPosition.ObjCellId);
|
||||||
|
Assert.False(body.InWorld);
|
||||||
|
Assert.False(receipt.LastMoveWasAutonomous);
|
||||||
|
Assert.False(body.LastMoveWasAutonomous);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NoMovementPayloadStagesTheDormantPlacementFrame()
|
||||||
|
{
|
||||||
|
(PhysicsBody body, RuntimeRemoteBodyConstructionReceipt receipt) =
|
||||||
|
ConstructDirect(movement: null);
|
||||||
|
|
||||||
|
Assert.False(receipt.MovementBranch);
|
||||||
|
Assert.True(receipt.PlacementFrameStaged);
|
||||||
|
Assert.Equal(Cell, body.CellPosition.ObjCellId);
|
||||||
|
// SetPlacementFrameInternal is NOT enter_world — the submission owns
|
||||||
|
// world residence.
|
||||||
|
Assert.False(body.InWorld);
|
||||||
|
Assert.False(body.LastMoveWasAutonomous);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Yield flavors + retry idempotency per stage
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AwaitingCollisionSourceRetriesThenResumesOnceSetupLands()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true, setupTableId: SetupId);
|
||||||
|
fixture.CollisionSource.Status = PreparedAssetReadStatus.Missing;
|
||||||
|
|
||||||
|
RuntimeRemoteFirstEntryStatus first = fixture.Advance(out _);
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingCollisionSource, first);
|
||||||
|
// No progress entry, no body, nothing mutated while the Setup is
|
||||||
|
// outstanding.
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
Assert.Null(fixture.Record.PhysicsBody);
|
||||||
|
|
||||||
|
RuntimeRemoteFirstEntryStatus second = fixture.Advance(out _);
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingCollisionSource, second);
|
||||||
|
Assert.True(fixture.CollisionSource.ReadCount >= 2);
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
Assert.Null(fixture.Record.PhysicsBody);
|
||||||
|
|
||||||
|
fixture.CollisionSource.Status = PreparedAssetReadStatus.Loaded;
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed,
|
||||||
|
fixture.Advance(out RuntimeInitialCreateExecutionReceipt receipt));
|
||||||
|
Assert.Equal(Cell, receipt.FullCellId);
|
||||||
|
Assert.NotNull(fixture.Record.PhysicsBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeferredPlacementRetainsOneBodyIdentityAcrossRetriesThenWakesAndCompletes()
|
||||||
|
{
|
||||||
|
// Non-resident world: the ordinary submission defers (the
|
||||||
|
// destination landblock's collision world was never added), parking
|
||||||
|
// the operation behind a Withdraw the conductor drains itself.
|
||||||
|
using var fixture = new Fixture(residentWorld: false);
|
||||||
|
|
||||||
|
RuntimeRemoteFirstEntryStatus first = fixture.Advance(out _);
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingPlacement, first);
|
||||||
|
PhysicsBody body = Assert.IsType<PhysicsBody>(fixture.Record.PhysicsBody);
|
||||||
|
Assert.False(body.InWorld);
|
||||||
|
Assert.Equal(0u, fixture.Record.FullCellId);
|
||||||
|
// The construction is still inspectable mid-flight, and the
|
||||||
|
// set_description writes are already on the body.
|
||||||
|
Assert.True(fixture.Conductor.TryGetConstruction(
|
||||||
|
fixture.Key, out RuntimeRemoteBodyConstructionReceipt construction));
|
||||||
|
Assert.True(construction.FrictionApplied);
|
||||||
|
Assert.Equal(0.5f, body.Friction);
|
||||||
|
Assert.True(construction.VelocityApplied);
|
||||||
|
Assert.Equal(new Vector3(1f, 2f, 0.5f), body.Velocity);
|
||||||
|
Assert.True(construction.OmegaApplied);
|
||||||
|
Assert.Equal(1, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
|
||||||
|
// Retry idempotency: the SAME body identity survives every retry —
|
||||||
|
// no duplicate construction, no re-submission churn.
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingPlacement,
|
||||||
|
fixture.Advance(out _));
|
||||||
|
Assert.Same(body, fixture.Record.PhysicsBody);
|
||||||
|
Assert.Equal(1, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ulong generation = 1UL;
|
||||||
|
fixture.Lifetime.Physics.SetPosition.BeginCollisionGeneration(
|
||||||
|
Landblock, generation);
|
||||||
|
fixture.Lifetime.Physics.Engine.AddLandblock(
|
||||||
|
Landblock,
|
||||||
|
new TerrainSurface(new byte[81], new float[256]),
|
||||||
|
Array.Empty<CellSurface>(),
|
||||||
|
Array.Empty<PortalPlane>(),
|
||||||
|
worldOffsetX: 0f,
|
||||||
|
worldOffsetY: 0f);
|
||||||
|
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
||||||
|
Landblock, generation, ready: true);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed,
|
||||||
|
fixture.Advance(out RuntimeInitialCreateExecutionReceipt receipt));
|
||||||
|
Assert.Same(body, fixture.Record.PhysicsBody);
|
||||||
|
Assert.True(body.InWorld);
|
||||||
|
Assert.Equal(Cell, receipt.FullCellId);
|
||||||
|
Assert.Equal(Cell, fixture.Record.FullCellId);
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReentrantAdvanceDuringCollisionCallbackFailsClosedWithContentionAndOuterCallStillCompletes()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
bool reentered = false;
|
||||||
|
RuntimeRemoteFirstEntryStatus? innerStatus = null;
|
||||||
|
fixture.Lifetime.Physics.Engine.TransitionCellCollisionTestHook =
|
||||||
|
(_, phase, _, observed) =>
|
||||||
|
{
|
||||||
|
if (!reentered && phase is TransitionCellCollisionPhase.Environment)
|
||||||
|
{
|
||||||
|
reentered = true;
|
||||||
|
innerStatus = fixture.Advance(out _);
|
||||||
|
}
|
||||||
|
return observed;
|
||||||
|
};
|
||||||
|
|
||||||
|
RuntimeRemoteFirstEntryStatus outerStatus = fixture.Advance(
|
||||||
|
out RuntimeInitialCreateExecutionReceipt receipt);
|
||||||
|
|
||||||
|
Assert.True(reentered);
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.Contention, innerStatus);
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed, outerStatus);
|
||||||
|
Assert.Equal(Cell, receipt.FullCellId);
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AwaitingReceiptAcknowledgementWhileAnotherEntityHoldsTheFifoHeadThenResumes()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
(RuntimeEntityRecord other, RuntimePlacementProjectionToken otherToken) =
|
||||||
|
BeginPendingOrdinaryPlacement(fixture, 0x70099001u);
|
||||||
|
|
||||||
|
// Our submission commits, but its Place sits BEHIND the other
|
||||||
|
// entity's unacknowledged receipt in the one ordered FIFO.
|
||||||
|
RuntimeRemoteFirstEntryStatus first = fixture.Advance(out _);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeRemoteFirstEntryStatus.AwaitingReceiptAcknowledgement,
|
||||||
|
first);
|
||||||
|
Assert.True(fixture.Record.PhysicsBody!.InWorld);
|
||||||
|
|
||||||
|
RuntimeRemoteFirstEntryStatus second = fixture.Advance(out _);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeRemoteFirstEntryStatus.AwaitingReceiptAcknowledgement,
|
||||||
|
second);
|
||||||
|
|
||||||
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
||||||
|
.AcknowledgeProjection(otherToken));
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed,
|
||||||
|
fixture.Advance(out RuntimeInitialCreateExecutionReceipt receipt));
|
||||||
|
Assert.Equal(Cell, receipt.FullCellId);
|
||||||
|
_ = other;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteWhileAwaitingReceiptAcknowledgementAbandonsInsteadOfRetryingForeverAndConverges()
|
||||||
|
{
|
||||||
|
// M2a — the exact C3a-shaped scenario: the placement has ALREADY
|
||||||
|
// committed (body in world) but acknowledgement is blocked behind
|
||||||
|
// another entity's unacknowledged Place. The delete-path mechanism
|
||||||
|
// (Physics.SetPosition.Forget with releasePreparedMover — the exact
|
||||||
|
// inner call TryAcceptDelete makes) rewrites our still-pending Place
|
||||||
|
// slot to Discard with a bumped revision, so the cached
|
||||||
|
// progress.Projection can never match the FIFO head again. Without
|
||||||
|
// the shared IsAcknowledgementStillPending re-check, Advance would
|
||||||
|
// report AwaitingReceiptAcknowledgement forever and ownership would
|
||||||
|
// never converge.
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
(RuntimeEntityRecord other, RuntimePlacementProjectionToken otherToken) =
|
||||||
|
BeginPendingOrdinaryPlacement(fixture, 0x70099002u);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeRemoteFirstEntryStatus.AwaitingReceiptAcknowledgement,
|
||||||
|
fixture.Advance(out _));
|
||||||
|
Assert.Equal(1, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
Assert.True(fixture.Record.PhysicsBody!.InWorld);
|
||||||
|
Assert.True(fixture.Lifetime.TryGetInitialCreateResidence(
|
||||||
|
fixture.Record, out _));
|
||||||
|
|
||||||
|
RuntimePlacementCancellationReceipt cancellation = fixture.Lifetime
|
||||||
|
.Physics.SetPosition.Forget(
|
||||||
|
fixture.Record,
|
||||||
|
releasePreparedMover: true);
|
||||||
|
fixture.Lifetime.Physics.SetPosition.PublishCancellation(cancellation);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.RejectedAuthority,
|
||||||
|
fixture.Advance(out _));
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
|
||||||
|
// The unrelated entity's own placement is untouched and still
|
||||||
|
// acknowledgeable — the abandonment never reached past our own
|
||||||
|
// entity's projection.
|
||||||
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
||||||
|
.AcknowledgeProjection(otherToken));
|
||||||
|
_ = other;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AwaitingContinuationPlacementPropagatesExecutorYieldThenResumes()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
// A fresh Position arrives while the initial residence is still
|
||||||
|
// pending — enqueued as a continuation, classified only at drain.
|
||||||
|
WorldSession.EntityPositionUpdate update = new(
|
||||||
|
fixture.Record.ServerGuid,
|
||||||
|
new CreateObject.ServerPosition(Cell, 40f, 20f, 7f, 1f, 0f, 0f, 0f),
|
||||||
|
Velocity: null,
|
||||||
|
PlacementId: 2,
|
||||||
|
IsGrounded: true,
|
||||||
|
InstanceSequence: 1,
|
||||||
|
PositionSequence: 2,
|
||||||
|
TeleportSequence: 1,
|
||||||
|
ForcePositionSequence: 0);
|
||||||
|
Assert.True(fixture.Lifetime.TryApplyPosition(
|
||||||
|
update,
|
||||||
|
isLocalPlayer: false,
|
||||||
|
forcePositionRotation: null,
|
||||||
|
currentLocalVelocity: null,
|
||||||
|
projectionRequiresTeleportHook: false,
|
||||||
|
acknowledgeProjection: null,
|
||||||
|
out PositionTimestampDisposition disposition,
|
||||||
|
out _,
|
||||||
|
out _));
|
||||||
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
||||||
|
|
||||||
|
RuntimeRemoteFirstEntryStatus status = fixture.Advance(out _);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeRemoteFirstEntryStatus.AwaitingContinuationPlacement,
|
||||||
|
status);
|
||||||
|
// The conductor's own sequence reached its terminal Acknowledged
|
||||||
|
// stage; the retained progress entry lets a retry skip straight to
|
||||||
|
// re-calling Execute.
|
||||||
|
Assert.Equal(1, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
|
||||||
|
RuntimeEntityKey key = fixture.Key;
|
||||||
|
Assert.True(fixture.Lifetime.InitialCreateExecution
|
||||||
|
.TryGetPendingContinuationPlacement(
|
||||||
|
key, out RuntimeEntityPlacementToken placement));
|
||||||
|
Assert.True(fixture.Lifetime.InitialCreateExecution
|
||||||
|
.TryGetPendingContinuationRoute(
|
||||||
|
key, out RuntimeAuthoritativePositionRoute route));
|
||||||
|
CompleteOrdinaryPlacement(fixture, placement, route);
|
||||||
|
|
||||||
|
RuntimeRemoteFirstEntryStatus resumed = fixture.Advance(
|
||||||
|
out RuntimeInitialCreateExecutionReceipt receipt);
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed, resumed);
|
||||||
|
Assert.Contains(receipt.Trace,
|
||||||
|
a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Domain refusal + never-clobber
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LocalPlayerLeaseIsRefusedWithoutTrackingAnything()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true, isLocalPlayer: true);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.RejectedToken,
|
||||||
|
fixture.Advance(out _));
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
Assert.Null(fixture.Record.PhysicsBody);
|
||||||
|
// The lease itself is untouched — the C3a conductor still owns it.
|
||||||
|
Assert.True(fixture.Lifetime.TryGetInitialCreateResidence(
|
||||||
|
fixture.Record, out RuntimeInitialCreateResidenceLease lease));
|
||||||
|
Assert.Equal(fixture.Lease.Token, lease.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ForeignBodyBoundOutOfBandAbandonsWithoutClobbering()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: true);
|
||||||
|
var foreign = new PhysicsBody
|
||||||
|
{
|
||||||
|
Position = new Vector3(1f, 2f, 3f),
|
||||||
|
State = fixture.Record.FinalPhysicsState,
|
||||||
|
};
|
||||||
|
fixture.Lifetime.Entities.SetPhysicsBody(fixture.Record, foreign);
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.RejectedAuthority,
|
||||||
|
fixture.Advance(out _));
|
||||||
|
// The foreign body was never replaced or mutated toward ours.
|
||||||
|
Assert.Same(foreign, fixture.Record.PhysicsBody);
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Delete / reset mid-flight convergence
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteWhileAwaitingPlacementConvergesAutomaticallyThroughTheRetirementFanOut()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: false);
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingPlacement,
|
||||||
|
fixture.Advance(out _));
|
||||||
|
Assert.Equal(1, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
|
||||||
|
DeleteEntity(fixture);
|
||||||
|
Assert.Null(fixture.Record.Key);
|
||||||
|
|
||||||
|
// TryAcceptDelete's ForgetInitialCreateResidence fired the multicast
|
||||||
|
// retirement notification synchronously — this conductor's Forget is
|
||||||
|
// bound into that same fan-out, so convergence is complete before
|
||||||
|
// delete returns.
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
|
||||||
|
// Retrying Advance afterward is a safe, distinct no-op.
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.RejectedToken,
|
||||||
|
fixture.Advance(out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SessionClearMidFlightConvergesOwnership()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: false);
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingPlacement,
|
||||||
|
fixture.Advance(out _));
|
||||||
|
Assert.Equal(1, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
|
||||||
|
_ = fixture.Lifetime.BeginSessionClear();
|
||||||
|
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
Assert.Equal(0, fixture.Lifetime.CaptureOwnership()
|
||||||
|
.RemoteFirstEntryActiveCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeleteAndSameGuidReincarnationStartsAFreshSequence()
|
||||||
|
{
|
||||||
|
using var fixture = new Fixture(residentWorld: false);
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingPlacement,
|
||||||
|
fixture.Advance(out _));
|
||||||
|
RuntimeEntityKey staleKey = fixture.Key;
|
||||||
|
uint guid = fixture.Record.ServerGuid;
|
||||||
|
|
||||||
|
DeleteEntity(fixture);
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
|
||||||
|
RuntimeEntityRecord reincarnated = fixture.Lifetime
|
||||||
|
.RegisterEntityWithInitialResidence(
|
||||||
|
Spawn(guid, incarnation: 2),
|
||||||
|
isLocalPlayer: false)
|
||||||
|
.Canonical!;
|
||||||
|
Assert.NotEqual(staleKey, reincarnated.Key!.Value);
|
||||||
|
Assert.True(fixture.Lifetime.TryGetInitialCreateResidence(
|
||||||
|
reincarnated,
|
||||||
|
out RuntimeInitialCreateResidenceLease freshLease));
|
||||||
|
fixture.Record = reincarnated;
|
||||||
|
fixture.Lease = freshLease;
|
||||||
|
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.AwaitingPlacement,
|
||||||
|
fixture.Advance(out _));
|
||||||
|
const ulong generation = 1UL;
|
||||||
|
fixture.Lifetime.Physics.SetPosition.BeginCollisionGeneration(
|
||||||
|
Landblock, generation);
|
||||||
|
fixture.Lifetime.Physics.Engine.AddLandblock(
|
||||||
|
Landblock,
|
||||||
|
new TerrainSurface(new byte[81], new float[256]),
|
||||||
|
Array.Empty<CellSurface>(),
|
||||||
|
Array.Empty<PortalPlane>(),
|
||||||
|
worldOffsetX: 0f,
|
||||||
|
worldOffsetY: 0f);
|
||||||
|
fixture.Lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
||||||
|
Landblock, generation, ready: true);
|
||||||
|
|
||||||
|
RuntimeRemoteFirstEntryStatus status = fixture.Advance(
|
||||||
|
out RuntimeInitialCreateExecutionReceipt receipt);
|
||||||
|
Assert.Equal(RuntimeRemoteFirstEntryStatus.Completed, status);
|
||||||
|
Assert.Equal(Cell, receipt.FullCellId);
|
||||||
|
Assert.NotNull(reincarnated.PhysicsBody);
|
||||||
|
Assert.True(reincarnated.PhysicsBody!.InWorld);
|
||||||
|
Assert.Equal(0, fixture.Conductor.CaptureOwnership().ActiveCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
private static (PhysicsBody Body, RuntimeRemoteBodyConstructionReceipt Receipt)
|
||||||
|
ConstructDirect(
|
||||||
|
float? friction = 0.5f,
|
||||||
|
float? elasticity = 0.05f,
|
||||||
|
float? translucency = null,
|
||||||
|
PhysicsMovementData? movement = null)
|
||||||
|
{
|
||||||
|
var record = new RuntimeEntityRecordFactory();
|
||||||
|
RuntimeEntityRecord canonical = record.Lifetime
|
||||||
|
.RegisterEntity(
|
||||||
|
Spawn(
|
||||||
|
0x70090077u,
|
||||||
|
incarnation: 1,
|
||||||
|
friction: friction,
|
||||||
|
elasticity: elasticity,
|
||||||
|
translucency: translucency,
|
||||||
|
movement: movement))
|
||||||
|
.Canonical!;
|
||||||
|
var command = new RuntimeSetPositionCommand(
|
||||||
|
new PhysicsSetPositionRequest(
|
||||||
|
new Vector3(1f, 2f, 3f),
|
||||||
|
Quaternion.Identity,
|
||||||
|
Cell,
|
||||||
|
new Vector3(1f, 2f, 3f),
|
||||||
|
ImmutableArray<FlatCollisionSphere>.Empty,
|
||||||
|
Scale: 1f,
|
||||||
|
StepUpHeight: 0f,
|
||||||
|
StepDownHeight: 0f,
|
||||||
|
canonical.FinalPhysicsState,
|
||||||
|
ObjectInfoState.None,
|
||||||
|
canonical.Key?.LocalEntityId ?? 0u,
|
||||||
|
PhysicsPlacementClass.Ordinary,
|
||||||
|
PhysicsSetPositionFlags.Placement | PhysicsSetPositionFlags.Slide),
|
||||||
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
||||||
|
GameTime: 1d,
|
||||||
|
ExpectedVelocityAuthorityVersion: 0UL);
|
||||||
|
|
||||||
|
PhysicsBody body = RuntimeRemoteBodyDescription.Construct(
|
||||||
|
canonical,
|
||||||
|
canonical.Snapshot.Physics,
|
||||||
|
command,
|
||||||
|
out RuntimeRemoteBodyConstructionReceipt receipt);
|
||||||
|
record.Dispose();
|
||||||
|
return (body, receipt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class RuntimeEntityRecordFactory : IDisposable
|
||||||
|
{
|
||||||
|
internal RuntimeEntityObjectLifetime Lifetime { get; } = new();
|
||||||
|
|
||||||
|
internal RuntimeEntityRecordFactory()
|
||||||
|
{
|
||||||
|
var generation = new RuntimeGenerationToken(1UL);
|
||||||
|
Lifetime.BindEventContext(() => generation, static () => 1UL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => Lifetime.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (RuntimeEntityRecord Record, RuntimePlacementProjectionToken Token)
|
||||||
|
BeginPendingOrdinaryPlacement(Fixture fixture, uint guid)
|
||||||
|
{
|
||||||
|
RuntimeEntityRecord record = fixture.Lifetime.RegisterEntity(
|
||||||
|
Spawn(guid, incarnation: 1)).Canonical!;
|
||||||
|
var body = new PhysicsBody
|
||||||
|
{
|
||||||
|
Position = new Vector3(50f, 50f, 3f),
|
||||||
|
Orientation = Quaternion.Identity,
|
||||||
|
State = record.FinalPhysicsState,
|
||||||
|
};
|
||||||
|
body.SnapToCell(Cell, body.Position, body.Position);
|
||||||
|
fixture.Lifetime.Entities.SetPhysicsBody(record, body);
|
||||||
|
RuntimeEntityPlacementToken placement = fixture.Lifetime.Physics
|
||||||
|
.SetPosition.BeginAuthoredPlacement(
|
||||||
|
record,
|
||||||
|
record.PositionAuthorityVersion,
|
||||||
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
||||||
|
Assert.True(placement.IsValid);
|
||||||
|
var preparation = new RuntimeSetPositionMoverPreparation(
|
||||||
|
RuntimeSetPositionMoverSetup.ResolvedAbsent,
|
||||||
|
RuntimeSetPositionOperationKind.RemoteAuthoritative,
|
||||||
|
GameTime: 1d,
|
||||||
|
PhysicsPlacementClass.Ordinary,
|
||||||
|
PhysicsSetPositionFlags.Placement | PhysicsSetPositionFlags.Slide);
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
|
||||||
|
fixture.Lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
placement, preparation, out RuntimeSetPositionCommand command));
|
||||||
|
RuntimeSetPositionOutcome outcome = fixture.Lifetime.Physics.SetPosition
|
||||||
|
.SubmitPreparedPlacement(placement, command);
|
||||||
|
Assert.Equal(RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
||||||
|
outcome.Status);
|
||||||
|
return (record, outcome.Projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CompleteOrdinaryPlacement(
|
||||||
|
Fixture fixture,
|
||||||
|
in RuntimeEntityPlacementToken placement,
|
||||||
|
in RuntimeAuthoritativePositionRoute route)
|
||||||
|
{
|
||||||
|
var preparation = new RuntimeSetPositionMoverPreparation(
|
||||||
|
RuntimeSetPositionMoverSetup.ResolvedAbsent,
|
||||||
|
route.OperationKind,
|
||||||
|
GameTime: 1d,
|
||||||
|
PhysicsPlacementClass.Ordinary,
|
||||||
|
route.SetPositionFlags);
|
||||||
|
Assert.Equal(RuntimeSetPositionMoverPreparationStatus.Prepared,
|
||||||
|
fixture.Lifetime.Physics.SetPosition.PrepareMover(
|
||||||
|
placement, preparation, out RuntimeSetPositionCommand command));
|
||||||
|
RuntimeSetPositionOutcome outcome = fixture.Lifetime.Physics.SetPosition
|
||||||
|
.SubmitPreparedPlacement(placement, command);
|
||||||
|
Assert.Equal(RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
||||||
|
outcome.Status);
|
||||||
|
Assert.True(fixture.Lifetime.Physics.SetPosition
|
||||||
|
.AcknowledgeProjection(outcome.Projection));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DeleteEntity(Fixture fixture)
|
||||||
|
{
|
||||||
|
Assert.True(fixture.Lifetime.TryAcceptDelete(
|
||||||
|
new DeleteObject.Parsed(
|
||||||
|
fixture.Record.ServerGuid, fixture.Record.Incarnation),
|
||||||
|
isLocalPlayer: false,
|
||||||
|
removeRetainedObject: false,
|
||||||
|
out RuntimeEntityDeleteAcceptance acceptance));
|
||||||
|
fixture.Lifetime.CompleteAcceptedDelete(acceptance);
|
||||||
|
Assert.Null(fixture.Lifetime.RetireCanonicalOnly(fixture.Record));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldSession.EntitySpawn Spawn(
|
||||||
|
uint guid,
|
||||||
|
ushort incarnation,
|
||||||
|
bool includePosition = true,
|
||||||
|
uint setupTableId = 0u,
|
||||||
|
uint rawState = (uint)(PhysicsStateFlags.Gravity
|
||||||
|
| PhysicsStateFlags.ReportCollisions),
|
||||||
|
float? friction = 0.5f,
|
||||||
|
float? elasticity = 0.05f,
|
||||||
|
float? translucency = null,
|
||||||
|
PhysicsMovementData? movement = null)
|
||||||
|
{
|
||||||
|
CreateObject.ServerPosition? position = includePosition
|
||||||
|
? new CreateObject.ServerPosition(Cell, 1f, 2f, 3f, 1f, 0f, 0f, 0f)
|
||||||
|
: null;
|
||||||
|
var timestamps = new PhysicsTimestamps(
|
||||||
|
Position: 1,
|
||||||
|
Movement: 1,
|
||||||
|
State: 1,
|
||||||
|
Vector: 1,
|
||||||
|
Teleport: 0,
|
||||||
|
ServerControlledMove: 1,
|
||||||
|
ForcePosition: 0,
|
||||||
|
ObjDesc: 1,
|
||||||
|
Instance: incarnation);
|
||||||
|
var physics = new PhysicsSpawnData(
|
||||||
|
RawState: rawState,
|
||||||
|
Position: position,
|
||||||
|
Movement: movement,
|
||||||
|
AnimationFrame: null,
|
||||||
|
SetupTableId: setupTableId == 0u ? null : setupTableId,
|
||||||
|
MotionTableId: 0x09000001u,
|
||||||
|
SoundTableId: null,
|
||||||
|
PhysicsScriptTableId: null,
|
||||||
|
Parent: null,
|
||||||
|
Children: null,
|
||||||
|
Scale: 1f,
|
||||||
|
Friction: friction,
|
||||||
|
Elasticity: elasticity,
|
||||||
|
Translucency: translucency,
|
||||||
|
Velocity: new Vector3(1f, 2f, 0.5f),
|
||||||
|
Acceleration: null,
|
||||||
|
AngularVelocity: new Vector3(0f, 0f, 0.25f),
|
||||||
|
DefaultScriptType: null,
|
||||||
|
DefaultScriptIntensity: null,
|
||||||
|
Timestamps: timestamps);
|
||||||
|
return new WorldSession.EntitySpawn(
|
||||||
|
Guid: guid,
|
||||||
|
Position: position,
|
||||||
|
SetupTableId: setupTableId == 0u ? null : setupTableId,
|
||||||
|
AnimPartChanges: Array.Empty<CreateObject.AnimPartChange>(),
|
||||||
|
TextureChanges: Array.Empty<CreateObject.TextureChange>(),
|
||||||
|
SubPalettes: Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||||
|
BasePaletteId: null,
|
||||||
|
ObjScale: 1f,
|
||||||
|
Name: "remote-entry-fixture",
|
||||||
|
ItemType: null,
|
||||||
|
MotionState: null,
|
||||||
|
MotionTableId: 0x09000001u,
|
||||||
|
PhysicsState: physics.RawState,
|
||||||
|
ObjectDescriptionFlags: 0x8u,
|
||||||
|
Friction: friction,
|
||||||
|
Elasticity: elasticity,
|
||||||
|
InstanceSequence: incarnation,
|
||||||
|
MovementSequence: 1,
|
||||||
|
ServerControlSequence: 1,
|
||||||
|
PositionSequence: 1,
|
||||||
|
Physics: physics);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeCollisionSource(
|
||||||
|
uint expectedSetupTableId,
|
||||||
|
FlatSetupCollision setup) : IPreparedCollisionSource
|
||||||
|
{
|
||||||
|
internal int ReadCount { get; private set; }
|
||||||
|
internal PreparedAssetReadStatus Status { get; set; } =
|
||||||
|
PreparedAssetReadStatus.Loaded;
|
||||||
|
|
||||||
|
public PreparedAssetPresence ProbeCollision(
|
||||||
|
PakAssetType type, uint sourceFileId) =>
|
||||||
|
PreparedAssetPresence.Available;
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatSetupCollision> ReadSetupCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ReadCount++;
|
||||||
|
Assert.Equal(expectedSetupTableId, sourceFileId);
|
||||||
|
return Status switch
|
||||||
|
{
|
||||||
|
PreparedAssetReadStatus.Loaded =>
|
||||||
|
PreparedCollisionReadResult<FlatSetupCollision>.Loaded(setup),
|
||||||
|
PreparedAssetReadStatus.Corrupt =>
|
||||||
|
PreparedCollisionReadResult<FlatSetupCollision>.Corrupt,
|
||||||
|
_ => PreparedCollisionReadResult<FlatSetupCollision>.Missing,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
|
||||||
|
ReadGfxObjCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
throw new NotSupportedException(
|
||||||
|
"Only ReadSetupCollision is exercised by these tests.");
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
|
||||||
|
ReadCellStructureCollision(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
throw new NotSupportedException(
|
||||||
|
"Only ReadSetupCollision is exercised by these tests.");
|
||||||
|
|
||||||
|
public PreparedCollisionReadResult<FlatEnvCellTopology>
|
||||||
|
ReadEnvCellTopology(
|
||||||
|
uint sourceFileId,
|
||||||
|
CancellationToken cancellationToken = default) =>
|
||||||
|
throw new NotSupportedException(
|
||||||
|
"Only ReadSetupCollision is exercised by these tests.");
|
||||||
|
|
||||||
|
public PreparedCollisionSourceStats CollisionStats => default;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Fixture : IDisposable
|
||||||
|
{
|
||||||
|
internal Fixture(
|
||||||
|
bool residentWorld,
|
||||||
|
uint setupTableId = 0u,
|
||||||
|
uint rawState = (uint)(PhysicsStateFlags.Gravity
|
||||||
|
| PhysicsStateFlags.ReportCollisions),
|
||||||
|
bool isLocalPlayer = false)
|
||||||
|
{
|
||||||
|
if (residentWorld)
|
||||||
|
{
|
||||||
|
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
|
||||||
|
engine.AddLandblock(
|
||||||
|
Landblock,
|
||||||
|
new TerrainSurface(new byte[81], new float[256]),
|
||||||
|
Array.Empty<CellSurface>(),
|
||||||
|
Array.Empty<PortalPlane>(),
|
||||||
|
worldOffsetX: 0f,
|
||||||
|
worldOffsetY: 0f);
|
||||||
|
Lifetime = new RuntimeEntityObjectLifetime(engine);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Lifetime = new RuntimeEntityObjectLifetime();
|
||||||
|
}
|
||||||
|
var generation = new RuntimeGenerationToken(1UL);
|
||||||
|
Lifetime.BindEventContext(() => generation, static () => 1UL);
|
||||||
|
|
||||||
|
// The SAME conductor instance RuntimeEntityObjectLifetime itself
|
||||||
|
// constructs and wires into the residence's multicast retirement
|
||||||
|
// fan-out and BeginSessionClear — never a standalone copy — so
|
||||||
|
// these tests exercise the real production wiring.
|
||||||
|
Conductor = Lifetime.RemoteFirstEntry;
|
||||||
|
|
||||||
|
Record = Lifetime.RegisterEntityWithInitialResidence(
|
||||||
|
Spawn(
|
||||||
|
0x70090002u,
|
||||||
|
incarnation: 1,
|
||||||
|
setupTableId: setupTableId,
|
||||||
|
rawState: rawState),
|
||||||
|
isLocalPlayer).Canonical!;
|
||||||
|
Assert.True(Lifetime.TryGetInitialCreateResidence(
|
||||||
|
Record, out RuntimeInitialCreateResidenceLease lease));
|
||||||
|
Lease = lease;
|
||||||
|
|
||||||
|
CollisionSource = new FakeCollisionSource(
|
||||||
|
setupTableId,
|
||||||
|
new FlatSetupCollision(
|
||||||
|
ImmutableArray<FlatCollisionCylinder>.Empty,
|
||||||
|
[new FlatCollisionSphere(Vector3.Zero, 0.48f)],
|
||||||
|
height: 0f,
|
||||||
|
radius: 0f,
|
||||||
|
stepUpHeight: 0.4f,
|
||||||
|
stepDownHeight: 0.4f));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal RuntimeEntityObjectLifetime Lifetime { get; }
|
||||||
|
internal RuntimeRemoteFirstEntryState Conductor { get; }
|
||||||
|
internal RuntimeEntityRecord Record { get; set; }
|
||||||
|
internal RuntimeInitialCreateResidenceLease Lease { get; set; }
|
||||||
|
internal FakeCollisionSource CollisionSource { get; }
|
||||||
|
|
||||||
|
internal RuntimeEntityKey Key => Record.Key!.Value;
|
||||||
|
|
||||||
|
internal RuntimeRemoteFirstEntryStatus Advance(
|
||||||
|
out RuntimeInitialCreateExecutionReceipt receipt) =>
|
||||||
|
Advance(out receipt, out _);
|
||||||
|
|
||||||
|
internal RuntimeRemoteFirstEntryStatus Advance(
|
||||||
|
out RuntimeInitialCreateExecutionReceipt receipt,
|
||||||
|
out RuntimeRemoteBodyConstructionReceipt construction) =>
|
||||||
|
Conductor.Advance(
|
||||||
|
Record,
|
||||||
|
Lease.Token,
|
||||||
|
CollisionSource,
|
||||||
|
gameTime: 10d,
|
||||||
|
NoContact,
|
||||||
|
out receipt,
|
||||||
|
out construction);
|
||||||
|
|
||||||
|
public void Dispose() => Lifetime.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue