Cutover slice C0 (docs/plans/2026-08-02-placement-cutover.md): the seam work that lets C3 flip hosts onto a complete receipt stream instead of growing one mid-cutover. The executor's Released exit now publishes an acknowledge-only ExecutorCompleted receipt through the one placement projection stream — registered before observer dispatch, correlated to the full execution receipt, reaped exactly once on acknowledgement/ discard/session-clear, and counted in the convergence ledger. All three production placement sinks acknowledge-and-ignore the new kind via early returns proven behavior-preserving for every existing kind; without them the first such receipt at cutover would permanently wedge the exact-head FIFO behind sinks that return false. Provably inert today: the publisher has no production caller. Execute's live inputs now derive from Runtime's own owners bound at GameRuntime construction: UsePositionFromServer is retail's exact autonomy_level != 2 (CommandInterpreter::UsePositionFromServer 0x006B3B40, startup-only knob), and PlayerDistance uses the live movement controller's position with a null-safe fallback to the caller struct — never a fabricated origin. TryPrepareAndSubmitAuthoredPlacement chains the prepared-collision Setup read through PrepareMover to submission with zero validation-semantics changes. TryCommitParent and CommitWithdrawal gain the sibling cancellation flow (residence + ordinary placement family); TryCommitParent deliberately omits LeaveWorld — retail's set_parent performs its single gated leave_world (0x00515A90) and a second would have no counterpart. Not fully dormant: the two cancellation fixes change Runtime paths production already calls (today as no-op-adjacent hardening, since nothing upstream begins a residence yet); everything else is reachable only by tests. Reviewed: retail-conformance PASS + architecture/ adversarial PASS after one fix round (sink wedge, completion-receipt lifecycle, null-controller distance). Runtime 921/921; complete Release solution 10,716 passed / 4 intentional skips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4946 lines
247 KiB
C#
4946 lines
247 KiB
C#
using System.Collections.Immutable;
|
|
using System.Numerics;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Physics;
|
|
|
|
namespace AcDream.Runtime.Tests.Entities;
|
|
|
|
public sealed class RuntimeInitialCreateContinuationExecutorTests
|
|
{
|
|
private const uint Landblock = 0xA9B50000u;
|
|
private const uint Cell = Landblock | 0x0001u;
|
|
private static readonly RuntimeInitialCreateExecutionInputs NoContact =
|
|
new(UsePositionFromServer: false, PlayerDistance: 0f);
|
|
|
|
// ---------------------------------------------------------------
|
|
// A. Basic
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void CompletedTopLevelCreateAdoptsOnceEmitsHookAndConvergesEveryLedger()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 1UL);
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(0x70020001u, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Equal(Cell, receipt.FullCellId);
|
|
Assert.Equal(RuntimeTeleportHookPhase.AfterEnterWorld, receipt.TeleportHookPhase);
|
|
Assert.Equal(0, receipt.ReplayedDeferredChildCount);
|
|
Assert.Equal(
|
|
[
|
|
RuntimeInitialCreateExecutedActionKind.InitialAdoption,
|
|
RuntimeInitialCreateExecutedActionKind.TeleportHookRequest,
|
|
],
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.Equal(
|
|
0,
|
|
lifetime.Physics.SetPosition.CaptureOwnership().AcknowledgedPlacementCompletionCount);
|
|
Assert.False(lifetime.TryGetInitialCreateResidence(canonical, out _));
|
|
|
|
// Retrying with the now-stale token is a distinct, safe no-op.
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedToken,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void MixedSimpleContinuationsDrainInExactSequenceOrderAndMutateSnapshot()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 2UL);
|
|
const uint parentGuid = 0x70021100u;
|
|
const uint guid = 0x70021000u;
|
|
_ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
Assert.False(lease.Placement.IsValid); // parented -> AwaitFreshPosition, no placement
|
|
|
|
var appearance = new ObjDescEvent.Parsed(
|
|
guid,
|
|
new CreateObject.ModelData(
|
|
0x04000002u,
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.AnimPartChange>()),
|
|
InstanceSequence: 1,
|
|
ObjDescSequence: 2);
|
|
Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
|
|
var vector = new VectorUpdate.Parsed(
|
|
guid, new Vector3(1f, 2f, 3f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
|
|
Assert.True(lifetime.TryApplyVector(vector, null, out _));
|
|
var state = new SetState.Parsed(
|
|
guid,
|
|
(uint)PhysicsStateFlags.Gravity,
|
|
InstanceSequence: 1,
|
|
StateSequence: 2);
|
|
Assert.True(lifetime.TryApplyState(state, null, out _, out _));
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
using IDisposable subscription = lifetime.Events.Subscribe(
|
|
new EntityObserver(delta => observed.Add(delta.Change)));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Equal(
|
|
[
|
|
RuntimeInitialCreateExecutedActionKind.InitialAdoption,
|
|
RuntimeInitialCreateExecutedActionKind.ObjDesc,
|
|
RuntimeInitialCreateExecutedActionKind.Vector,
|
|
RuntimeInitialCreateExecutedActionKind.State,
|
|
],
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
Assert.Equal([1UL, 2UL, 3UL],
|
|
receipt.Trace
|
|
.Where(static a => a.Sequence != 0UL)
|
|
.Select(static a => a.Sequence));
|
|
Assert.Equal(
|
|
[RuntimeEntityChange.Updated, RuntimeEntityChange.Updated, RuntimeEntityChange.Updated],
|
|
observed);
|
|
Assert.Equal(0x04000002u, canonical.Snapshot.BasePaletteId);
|
|
Assert.Equal(new Vector3(1f, 2f, 3f), canonical.Snapshot.Physics!.Value.Velocity);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// Round 4 R4-4 (mandated regression test): a FIFO of ObjDesc then Vector
|
|
// - NEITHER touches any of the four executor-tracked baseline fields
|
|
// (PositionAuthorityVersion/CreateIntegrationVersion/FullCellId/
|
|
// PlacementCommitVersion), so NEITHER may call AdvanceExecutorBaseline
|
|
// at all. An observer bumps PositionAuthorityVersion externally during
|
|
// ObjDesc's own publish; because ObjDesc's apply never rebaselines that
|
|
// field, the STALE Expected value survives into the next check. The
|
|
// drain must detect this external race at ConsumeExecuted (the "next
|
|
// step") and abandon - NOT silently absorb it and report Released, which
|
|
// a blanket four-field rebaseline (the pre-R4-4 shape) would have done.
|
|
[Fact]
|
|
public void FieldMaskedBaselinePrecisionDetectsAnExternalPositionRaceDuringAnUnrelatedObjDescPublish()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 96UL);
|
|
const uint parentGuid = 0x70038000u;
|
|
const uint guid = 0x70038001u;
|
|
_ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
var appearance = new ObjDescEvent.Parsed(
|
|
guid,
|
|
new CreateObject.ModelData(
|
|
0x07000002u,
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.AnimPartChange>()),
|
|
InstanceSequence: 1,
|
|
ObjDescSequence: 2);
|
|
Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
|
|
var vector = new VectorUpdate.Parsed(
|
|
guid, new Vector3(1f, 2f, 3f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
|
|
Assert.True(lifetime.TryApplyVector(vector, null, out _));
|
|
|
|
bool bumped = false;
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
if (bumped || delta.Change is not RuntimeEntityChange.Updated)
|
|
return;
|
|
bumped = true;
|
|
// External, non-executor mutation of PositionAuthorityVersion
|
|
// during the ObjDesc stage's OWN publish.
|
|
lifetime.Entities.AdvancePositionAuthority(canonical);
|
|
}));
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedAuthority,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
|
|
Assert.True(bumped);
|
|
Assert.Equal(default, receipt);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// Round 3 A1 (mandated regression test): the executor's applies must
|
|
// write InboundPhysicsStateController's OWN _snapshots[guid] - the
|
|
// legacy merge base - in lockstep with RuntimeEntityRecord.Snapshot. If
|
|
// they only wrote the record's own Snapshot (as before A1), the FIRST
|
|
// legacy wire apply reached after the residence drain would re-merge
|
|
// against a STALE _snapshots[guid] base and silently revert every fact
|
|
// the drain just committed.
|
|
[Fact]
|
|
public void DrainedAppearanceAndPoseSurviveTheNextLegacyWireApply()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 30UL);
|
|
const uint parentGuid = 0x7002D100u;
|
|
const uint guid = 0x7002D000u;
|
|
_ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
// Drain an ObjDesc continuation that changes appearance (a fresh
|
|
// BasePaletteId) as part of the residence FIFO.
|
|
var appearance = new ObjDescEvent.Parsed(
|
|
guid,
|
|
new CreateObject.ModelData(
|
|
0x06000002u,
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.AnimPartChange>()),
|
|
InstanceSequence: 1,
|
|
ObjDescSequence: 2);
|
|
Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
Assert.Contains(
|
|
RuntimeInitialCreateExecutedActionKind.ObjDesc,
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
Assert.Equal(0x06000002u, canonical.Snapshot.BasePaletteId);
|
|
|
|
// Now run an ORDINARY legacy wire apply (Vector) on a channel the
|
|
// drain never touched. Its merge base is InboundPhysicsStateController's
|
|
// OWN _snapshots[guid] - if that store still held the pre-drain
|
|
// appearance (A1's bug), this apply's `old with { ... }` merge would
|
|
// carry the STALE BasePaletteId back into canonical.Snapshot via
|
|
// RefreshSnapshot, silently reverting the drained fact.
|
|
var vector = new VectorUpdate.Parsed(
|
|
guid, new Vector3(4f, 5f, 6f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
|
|
Assert.True(lifetime.TryApplyVector(vector, null, out _));
|
|
|
|
Assert.Equal(0x06000002u, canonical.Snapshot.BasePaletteId);
|
|
Assert.Equal(new Vector3(4f, 5f, 6f), canonical.Snapshot.Physics!.Value.Velocity);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// Round 3 B12: the WeenieDescription tail action must drive the object
|
|
// table the same way RuntimeLiveEntitySessionController.OnSpawned does
|
|
// for the non-residence direct-host Create path (ApplyAcceptedSpawn) -
|
|
// the prior "zero callers" claim for this wiring was false. A
|
|
// residence-pending admission deliberately never wires the object table
|
|
// at the initial Create (RegisterEntityCore's beginInitialResidence
|
|
// branch); the FIRST time this guid's entry can appear is exactly here.
|
|
[Fact]
|
|
public void WeenieDescriptionStageWiresTheObjectTableExactlyOnce()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 34UL);
|
|
const uint guid = 0x7002E400u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
int objectCountBeforeDrain = lifetime.Objects.ObjectCount;
|
|
|
|
WorldSession.EntitySpawn sameCreate = Spawn(
|
|
guid, 1, includePosition: false, positionSequence: 2);
|
|
PhysicsSpawnData physics = sameCreate.Physics!.Value;
|
|
sameCreate = sameCreate with
|
|
{
|
|
Physics = physics with
|
|
{
|
|
Timestamps = physics.Timestamps with { State = 2, Vector = 2 },
|
|
},
|
|
};
|
|
_ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Contains(
|
|
RuntimeInitialCreateExecutedActionKind.WeenieDescription,
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
Assert.Equal(objectCountBeforeDrain + 1, lifetime.Objects.ObjectCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// Round 4 R4-3: ApplyWeenieDescriptionAction's object-table apply window.
|
|
// ClientObjectTable.Ingest publishes ObjectAdded/ObjectUpdated
|
|
// SYNCHRONOUSLY - a subscriber may re-enter a wire apply for the SAME
|
|
// entity from inside that dispatch. Because AdvanceExecutorBaseline now
|
|
// runs BEFORE _applyAcceptedSpawn (not after), the residence's baseline
|
|
// is already current at the moment the reentrant call runs; the
|
|
// residence is not retired and the envelope still completes.
|
|
[Fact]
|
|
public void ObjectTableSubscriberReenteringAWireApplyDuringIngestDoesNotRetireTheResidenceAndTheEnvelopeCompletes()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 94UL);
|
|
const uint guid = 0x70037000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
WorldSession.EntitySpawn sameCreate = Spawn(
|
|
guid, 1, includePosition: false, positionSequence: 2);
|
|
PhysicsSpawnData physics = sameCreate.Physics!.Value;
|
|
sameCreate = sameCreate with
|
|
{
|
|
Physics = physics with
|
|
{
|
|
Timestamps = physics.Timestamps with { State = 2, Vector = 2 },
|
|
},
|
|
};
|
|
_ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
|
|
|
|
bool reentered = false;
|
|
lifetime.Objects.ObjectAdded += addedObject =>
|
|
{
|
|
if (reentered)
|
|
return;
|
|
reentered = true;
|
|
// A reentrant wire apply for a channel this envelope does NOT
|
|
// itself carry a dedicated stage for at this admission
|
|
// (Vector IS one of this envelope's own stages here, but the
|
|
// residence is still "pending" mid-drain, so this legitimately
|
|
// defers into the SAME FIFO rather than applying immediately -
|
|
// proving the residence survives the reentrant call).
|
|
var vector = new VectorUpdate.Parsed(
|
|
guid, new Vector3(9f, 8f, 7f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 3);
|
|
Assert.True(lifetime.TryApplyVector(vector, null, out _));
|
|
};
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.True(reentered);
|
|
Assert.Contains(
|
|
RuntimeInitialCreateExecutedActionKind.WeenieDescription,
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// Round 4 R4-3: a NESTED REPLACEMENT (a newer incarnation Create for the
|
|
// SAME guid) arriving from within Ingest's own synchronous dispatch
|
|
// invalidates the exact canonical incarnation the drain is still
|
|
// executing against. ApplyAcceptedSpawn re-checks currency AFTER its own
|
|
// object-table apply (mirroring RuntimeLiveEntitySessionController.cs:87's
|
|
// gate on that same call's result) and returns false; the executor must
|
|
// treat this as a typed abandonment - the remaining tail (ResidentCellCleanup)
|
|
// never runs.
|
|
[Fact]
|
|
public void NestedReplacementDuringObjectTableIngestAbandonsTheWeenieDescriptionStageWithNoFurtherStages()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 95UL);
|
|
const uint guid = 0x70037100u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
WorldSession.EntitySpawn sameCreate = Spawn(
|
|
guid, 1, includePosition: false, positionSequence: 2);
|
|
PhysicsSpawnData physics = sameCreate.Physics!.Value;
|
|
sameCreate = sameCreate with
|
|
{
|
|
Physics = physics with
|
|
{
|
|
Timestamps = physics.Timestamps with { State = 2, Vector = 2 },
|
|
},
|
|
};
|
|
_ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
|
|
|
|
bool reentered = false;
|
|
lifetime.Objects.ObjectAdded += addedObject =>
|
|
{
|
|
if (reentered)
|
|
return;
|
|
reentered = true;
|
|
_ = lifetime.RegisterEntity(Spawn(guid, 2, includePosition: false));
|
|
};
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedAuthority,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
|
|
Assert.True(reentered);
|
|
Assert.Equal(default, receipt);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// Round 3 A2 (mandated regression test): a standalone ObjDesc
|
|
// continuation admits a fresh palette (bumping the gate's ObjDesc
|
|
// channel and proving A1's snapshot lockstep feeds entry 2's merge
|
|
// base), THEN a same-incarnation Create arrives whose OWN raw
|
|
// WeenieDescription packet carries a DIFFERENT MotionTableId. Appearance
|
|
// fields (BasePaletteId etc.) are legitimately re-applied by the
|
|
// envelope's OWN dedicated ObjDesc stage (retail-faithful: a same-
|
|
// generation Create's tail always re-runs its own ObjDesc first) - that
|
|
// is NOT what A2 fixes. MotionTableId has NO dedicated envelope stage;
|
|
// WeenieDescription's merge is the ONLY place it can move, which
|
|
// isolates MergeUntimestampedCreate's "retained wins" rule cleanly: the
|
|
// prior wholesale RefreshSnapshot bug would have silently overwritten it
|
|
// with the incoming raw packet's value instead.
|
|
[Fact]
|
|
public void SameIncarnationWeenieDescriptionMergePreservesRetainedMotionTableId()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 31UL);
|
|
const uint parentGuid = 0x7002D300u;
|
|
const uint guid = 0x7002D200u;
|
|
_ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
|
|
|
|
WorldSession.EntitySpawn initial = Spawn(guid, 1, includePosition: false, parentGuid: parentGuid);
|
|
PhysicsSpawnData initialPhysics = initial.Physics!.Value;
|
|
initial = initial with
|
|
{
|
|
MotionTableId = 0x12345678u,
|
|
Physics = initialPhysics with { MotionTableId = 0x12345678u },
|
|
};
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(initial, isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
// FIFO entry 1: standalone ObjDesc with a NEW palette, bumping the
|
|
// gate's ObjDesc channel to 2.
|
|
var newAppearance = new ObjDescEvent.Parsed(
|
|
guid,
|
|
new CreateObject.ModelData(
|
|
0x07000002u,
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.AnimPartChange>()),
|
|
InstanceSequence: 1,
|
|
ObjDescSequence: 2);
|
|
Assert.True(lifetime.TryApplyObjDesc(newAppearance, null, out _));
|
|
|
|
// FIFO entry 2: a same-incarnation Create whose OWN raw
|
|
// WeenieDescription packet carries a DIFFERENT MotionTableId and a
|
|
// NEWER-still ObjDesc channel stamp (3).
|
|
WorldSession.EntitySpawn sameCreate = Spawn(
|
|
guid, 1, includePosition: false, parentGuid: parentGuid, positionSequence: 2);
|
|
PhysicsSpawnData samePhysics = sameCreate.Physics!.Value;
|
|
sameCreate = sameCreate with
|
|
{
|
|
MotionTableId = 0x99999999u,
|
|
Physics = samePhysics with
|
|
{
|
|
MotionTableId = 0x99999999u,
|
|
Timestamps = samePhysics.Timestamps with { ObjDesc = 3, State = 2, Vector = 2 },
|
|
},
|
|
};
|
|
RuntimeEntityRegistrationResult same = lifetime
|
|
.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
|
|
Assert.Equal(CreateObjectTimestampDisposition.ExistingGeneration, same.Inbound.Disposition);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Contains(
|
|
RuntimeInitialCreateExecutedActionKind.WeenieDescription,
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
// The envelope's OWN ObjDesc stage correctly re-applies THIS
|
|
// Create's own appearance (sameCreate never set its own palette, so
|
|
// it legitimately reverts to null - retail-faithful, not what A2
|
|
// fixes). Entry 1's transient fresh palette was always going to be
|
|
// superseded by entry 2's OWN ObjDesc stage; that stage - not
|
|
// WeenieDescription - owns this field.
|
|
Assert.Null(canonical.Snapshot.BasePaletteId);
|
|
// MotionTableId has no dedicated envelope stage - WeenieDescription's
|
|
// merge must keep the RETAINED value, never adopt incoming's raw
|
|
// packet wholesale.
|
|
Assert.Equal(0x12345678u, canonical.Snapshot.Physics!.Value.MotionTableId);
|
|
Assert.Equal(0x12345678u, canonical.Snapshot.MotionTableId);
|
|
// ObjDesc's timestamp DOES have a dedicated stage that legitimately
|
|
// advances it further as part of entry 2's own admission (unlike
|
|
// MotionTableId).
|
|
Assert.Equal(3, canonical.Snapshot.Physics!.Value.Timestamps.ObjDesc);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// B. Standalone continuations (Movement/Pickup/Parent) + enqueue-
|
|
// during-drain (ConsumeExecuted's Revised arm).
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void StandaloneMovementContinuationAppliesPayloadAndPublishesUpdated()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 40UL);
|
|
const uint guid = 0x7002F000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
var motion = new WorldSession.EntityMotionUpdate(
|
|
guid,
|
|
new CreateObject.ServerMotionState(0x3d, 0x11),
|
|
InstanceSequence: 1,
|
|
MovementSequence: 2,
|
|
ServerControlSequence: 1,
|
|
IsAutonomous: false);
|
|
// Movement (2) advances and ServerControl (1) is equal-not-stale, so
|
|
// the gate accepts the payload outright.
|
|
Assert.True(lifetime.TryApplyMotion(motion, retainPayload: true, null, out _, out _));
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
using IDisposable subscription = lifetime.Events.Subscribe(
|
|
new EntityObserver(delta => observed.Add(delta.Change)));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Equal(
|
|
[
|
|
RuntimeInitialCreateExecutedActionKind.InitialAdoption,
|
|
RuntimeInitialCreateExecutedActionKind.Movement,
|
|
],
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
Assert.Equal(new CreateObject.ServerMotionState(0x3d, 0x11),
|
|
canonical.Snapshot.MotionState);
|
|
Assert.Equal(2, canonical.Snapshot.Physics!.Value.Timestamps.Movement);
|
|
Assert.Equal([RuntimeEntityChange.Updated], observed);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void StandaloneMovementContinuationTimestampOnlyStampsWithoutPayloadOrPublish()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 41UL);
|
|
const uint guid = 0x7002F100u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
Assert.Null(canonical.Snapshot.MotionState);
|
|
|
|
var motion = new WorldSession.EntityMotionUpdate(
|
|
guid,
|
|
new CreateObject.ServerMotionState(0x3d, 0x11),
|
|
InstanceSequence: 1,
|
|
MovementSequence: 2,
|
|
// Retail consumes MOVEMENT_TS before discovering a stale
|
|
// SERVER_CONTROLLED_MOVE_TS (0, older than the gate's seeded 1) -
|
|
// Movement itself still advances (hasTimestampMutation) even
|
|
// though the overall event, and thus the payload, is rejected.
|
|
ServerControlSequence: 0,
|
|
IsAutonomous: false);
|
|
Assert.False(lifetime.TryApplyMotion(motion, retainPayload: true, null, out _, out _));
|
|
Assert.True(lifetime.InitialCreateResidences.TryGetTransaction(
|
|
canonical, out RuntimeInitialCreateResidenceLease retained));
|
|
Assert.Single(retained.Continuations);
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
using IDisposable subscription = lifetime.Events.Subscribe(
|
|
new EntityObserver(delta => observed.Add(delta.Change)));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Contains(
|
|
RuntimeInitialCreateExecutedActionKind.Movement,
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
// Timestamp landed; no payload, no publish (matches the legacy
|
|
// timestamp-only branch).
|
|
Assert.Equal(2, canonical.Snapshot.Physics!.Value.Timestamps.Movement);
|
|
Assert.Null(canonical.Snapshot.MotionState);
|
|
Assert.Empty(observed);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void StandalonePickupContinuationLeavesWorldThroughTheDrainAndResidenceStillReleases()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 42UL);
|
|
const uint guid = 0x7002F200u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
Assert.Equal(Cell, canonical.FullCellId);
|
|
|
|
// A committed parent projection so Pickup's own EndChildProjection
|
|
// has something real to tear down.
|
|
var relation = new ParentAttachmentRelation(
|
|
0x7002F300u, guid, ParentLocation: 1u, PlacementId: 0u,
|
|
ParentInstanceSequence: 1, ChildPositionSequence: 1);
|
|
lifetime.Entities.ParentAttachments.AcceptCreateObjectRelation(relation);
|
|
Assert.True(lifetime.Entities.ParentAttachments.CommitProjection(relation));
|
|
Assert.True(lifetime.Entities.ParentAttachments.HasCommittedParent(guid));
|
|
|
|
Assert.True(lifetime.TryApplyPickup(
|
|
new PickupEvent.Parsed(guid, InstanceSequence: 1, PositionSequence: 2),
|
|
null,
|
|
out _));
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
using IDisposable subscription = lifetime.Events.Subscribe(
|
|
new EntityObserver(delta => observed.Add(delta.Change)));
|
|
ulong clockEpochBefore = canonical.ObjectClockEpoch;
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Equal(
|
|
[
|
|
RuntimeInitialCreateExecutedActionKind.InitialAdoption,
|
|
RuntimeInitialCreateExecutedActionKind.Pickup,
|
|
],
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
Assert.Equal([RuntimeEntityChange.Withdrawn], observed);
|
|
Assert.Equal(0u, canonical.FullCellId);
|
|
// SuspendObjectClock bumps the epoch - the clock is no longer live.
|
|
Assert.NotEqual(clockEpochBefore, canonical.ObjectClockEpoch);
|
|
Assert.False(lifetime.Entities.ParentAttachments.HasCommittedParent(guid));
|
|
Assert.False(lifetime.Entities.ParentAttachments.TryGetRecoveryProjection(guid, out _));
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
// Residence still releases cleanly even though this continuation
|
|
// left the world mid-drain.
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void StandaloneParentContinuationAppliesPositionTimestampOnlyMergeWithNoReDefer()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 43UL);
|
|
const uint parentGuid = 0x7002F400u;
|
|
const uint childGuid = 0x7002F500u;
|
|
_ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
var parentUpdate = new ParentEvent.Parsed(
|
|
parentGuid,
|
|
childGuid,
|
|
ParentLocation: 1u,
|
|
PlacementId: 0u,
|
|
ParentInstanceSequence: 1,
|
|
ChildPositionSequence: 2);
|
|
// The parent is alive at admission AND stays alive all the way
|
|
// through the drain - contrast with
|
|
// ParentContinuationRevalidatesLiveParentAtExecutionAndReDefersOnMismatch,
|
|
// which deletes the parent between admission and execution.
|
|
Assert.True(lifetime.TryApplyParent(parentUpdate, null, out _));
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
using IDisposable subscription = lifetime.Events.Subscribe(
|
|
new EntityObserver(delta => observed.Add(delta.Change)));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Equal(
|
|
[
|
|
RuntimeInitialCreateExecutedActionKind.InitialAdoption,
|
|
RuntimeInitialCreateExecutedActionKind.Parent,
|
|
],
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
// ApplyAcceptedParent is position-timestamp-only: the shared
|
|
// POSITION_TS channel advances but no pose/ParentGuid field moves
|
|
// (the actual attach commit is TryCommitParent's job, out of scope
|
|
// here).
|
|
Assert.Equal(2, canonical.Snapshot.Physics!.Value.Timestamps.Position);
|
|
Assert.Null(canonical.Snapshot.ParentGuid);
|
|
Assert.Equal([RuntimeEntityChange.Updated], observed);
|
|
// The successful path hands NOTHING to ParentAttachments' unresolved
|
|
// bucket - the direct contrast with the re-defer test's
|
|
// UnresolvedRelationCount == 1.
|
|
Assert.Equal(0, lifetime.Entities.ParentAttachments.UnresolvedRelationCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// Round 3 arc-10 (enqueue-during-drain): a continuation enqueued from an
|
|
// observer mid-drain bumps the completed entry's Adoption.Revision in
|
|
// place; ConsumeExecuted must return Revised (not Released) so the SAME
|
|
// Execute() call's outer while(true) loop re-fetches via Complete and
|
|
// drains only the newly-appended tail. The already-applied prefix must
|
|
// never replay.
|
|
[Fact]
|
|
public void EnqueueDuringDrainExercisesConsumeExecutedRevisedArmAndDrainsTailOnlyOnce()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 44UL);
|
|
const uint guid = 0x7002F600u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
var appearance = new ObjDescEvent.Parsed(
|
|
guid,
|
|
new CreateObject.ModelData(
|
|
0x08000002u,
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.AnimPartChange>()),
|
|
InstanceSequence: 1,
|
|
ObjDescSequence: 2);
|
|
Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
bool enqueuedFromObserver = false;
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
observed.Add(delta.Change);
|
|
if (enqueuedFromObserver)
|
|
return;
|
|
enqueuedFromObserver = true;
|
|
// Reentrant enqueue, mid-drain, from inside the ObjDesc
|
|
// continuation's own publish: the residence is still in
|
|
// RuntimeInitialCreateResidenceState's _completed dictionary
|
|
// (not yet ConsumeExecuted'd), so this routes through
|
|
// CanEnqueue's completed-entry branch and bumps Revision in
|
|
// place.
|
|
var vector = new VectorUpdate.Parsed(
|
|
guid, new Vector3(7f, 8f, 9f), Vector3.Zero,
|
|
InstanceSequence: 1, VectorSequence: 2);
|
|
Assert.True(lifetime.TryApplyVector(vector, null, out _));
|
|
}));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
// Each action appears exactly once - the Revised loop-around must
|
|
// not replay the already-applied ObjDesc prefix.
|
|
Assert.Equal(
|
|
[
|
|
RuntimeInitialCreateExecutedActionKind.InitialAdoption,
|
|
RuntimeInitialCreateExecutedActionKind.ObjDesc,
|
|
RuntimeInitialCreateExecutedActionKind.Vector,
|
|
],
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
Assert.Equal(
|
|
[RuntimeEntityChange.Updated, RuntimeEntityChange.Updated],
|
|
observed);
|
|
Assert.Equal(0x08000002u, canonical.Snapshot.BasePaletteId);
|
|
Assert.Equal(new Vector3(7f, 8f, 9f), canonical.Snapshot.Physics!.Value.Velocity);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// C. Envelope atomicity
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void SameIncarnationEnvelopePublishesNothingUntilEveryStageCommitsThenPublishesInStageOrder()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 3UL);
|
|
const uint guid = 0x70022000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
WorldSession.EntitySpawn sameCreate = Spawn(
|
|
guid, 1, includePosition: false, positionSequence: 2) with
|
|
{
|
|
Name = "same-incarnation",
|
|
};
|
|
PhysicsSpawnData physics = sameCreate.Physics!.Value;
|
|
sameCreate = sameCreate with
|
|
{
|
|
Physics = physics with
|
|
{
|
|
Timestamps = physics.Timestamps with { ObjDesc = 2, State = 2, Vector = 2 },
|
|
},
|
|
};
|
|
RuntimeEntityRegistrationResult same = lifetime
|
|
.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
|
|
Assert.Equal(CreateObjectTimestampDisposition.ExistingGeneration, same.Inbound.Disposition);
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
int countAtFirstObservation = -1;
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
observed.Add(delta.Change);
|
|
countAtFirstObservation = observed.Count;
|
|
}));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
// No event fired until the whole envelope committed: the very first
|
|
// observation must already carry every publish, not a partial subset
|
|
// trickling in one at a time.
|
|
Assert.True(observed.Count >= 1);
|
|
Assert.Equal(observed.Count, countAtFirstObservation);
|
|
|
|
RuntimeInitialCreateExecutedActionKind[] envelopeStages = receipt.Trace
|
|
.Where(static a => a.Sequence == 1UL)
|
|
.Select(static a => a.Kind)
|
|
.ToArray();
|
|
// A same-Create with neither a parent nor a position decomposes into
|
|
// a Pickup branch (retail priority: Parent > Position > Pickup - see
|
|
// InboundPhysicsStateController.BuildSameGenerationEvents), plus the
|
|
// AP-119 PreTailDescriptionAdaptation stage this envelope always
|
|
// carries when SameGenerationEvents is present.
|
|
Assert.Equal(
|
|
[
|
|
RuntimeInitialCreateExecutedActionKind.PreTailDescriptionAdaptation,
|
|
RuntimeInitialCreateExecutedActionKind.ObjDesc,
|
|
RuntimeInitialCreateExecutedActionKind.Pickup,
|
|
RuntimeInitialCreateExecutedActionKind.State,
|
|
RuntimeInitialCreateExecutedActionKind.Vector,
|
|
RuntimeInitialCreateExecutedActionKind.WeenieDescription,
|
|
RuntimeInitialCreateExecutedActionKind.ResidentCellCleanup,
|
|
],
|
|
envelopeStages);
|
|
// This entity never claimed a cell (PickedUp residence, then the
|
|
// envelope's own Pickup stage nulls the position again) - Gap 4(c),
|
|
// the no-cell-claimed destruction-mark disposition.
|
|
RuntimeInitialCreateExecutedAction cleanup = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ResidentCellCleanup);
|
|
Assert.Equal(
|
|
RuntimeResidentCellCleanupDisposition.CelllessNoWeenieMarkUnreachable,
|
|
cleanup.ResidentCellCleanupDisposition);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResidentCellCleanupUnmarksWhenCellClaimedAndAlreadyResident()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 18UL);
|
|
const uint guid = 0x7002A000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
Assert.Equal(Cell, canonical.FullCellId);
|
|
|
|
// A same-Create carrying its own position: PositionSource ==
|
|
// SameIncarnationCreate makes effectiveContact always true for the
|
|
// non-local classify branch, and the entity is already resident
|
|
// (FullCellId == Cell, unchanged since neither Position's own
|
|
// refreshPosition:false merge nor WeenieDescription's touch it), so
|
|
// this classifies to Interpolate - no new placement, no yield.
|
|
WorldSession.EntitySpawn sameCreate = Spawn(guid, 1, positionSequence: 2, positionX: 15f);
|
|
PhysicsSpawnData physics = sameCreate.Physics!.Value;
|
|
sameCreate = sameCreate with
|
|
{
|
|
Physics = physics with
|
|
{
|
|
Timestamps = physics.Timestamps with { ObjDesc = 2 },
|
|
},
|
|
};
|
|
_ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
|
|
|
|
var inputs = new RuntimeInitialCreateExecutionInputs(
|
|
UsePositionFromServer: false, PlayerDistance: 10f);
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, inputs);
|
|
|
|
RuntimeInitialCreateExecutedAction cleanup = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ResidentCellCleanup);
|
|
Assert.Equal(
|
|
RuntimeResidentCellCleanupDisposition.ResidentUnmarked,
|
|
cleanup.ResidentCellCleanupDisposition);
|
|
Assert.Equal(Cell, canonical.FullCellId);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResidentCellCleanupFailsClosedWhenClaimedCelllessOutcomeIsNotUnderLostCellOwnership()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 19UL);
|
|
const uint guid = 0x7002A100u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
Assert.False(lease.Placement.IsValid);
|
|
Assert.Equal(0u, canonical.FullCellId);
|
|
|
|
// The same-Create's own position exists on the wire (claims a
|
|
// cell), but with UsePositionFromServer=false this LocalPlayer
|
|
// classify branch resolves to NoPositionOperation - no SetPosition
|
|
// ever begins, so nothing tracks a lost-cell/deferred operation
|
|
// for this entity either. The entity was never resident (PickedUp
|
|
// initial, no placement ever ran) - claimed + celless + NOT under
|
|
// lost-cell ownership is the exact invariant violation this test
|
|
// requires. Round 3 B1: this is now a typed abandonment (Abandon
|
|
// retires the residence and discards progress), never a throw
|
|
// escaping Execute - the ledger must still fully converge.
|
|
WorldSession.EntitySpawn sameCreate = Spawn(guid, 1, positionSequence: 2, positionX: 15f);
|
|
PhysicsSpawnData physics = sameCreate.Physics!.Value;
|
|
sameCreate = sameCreate with
|
|
{
|
|
Physics = physics with
|
|
{
|
|
Timestamps = physics.Timestamps with { ObjDesc = 2 },
|
|
},
|
|
};
|
|
_ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: true);
|
|
|
|
var inputs = new RuntimeInitialCreateExecutionInputs(
|
|
UsePositionFromServer: false, PlayerDistance: 0f);
|
|
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, inputs, out RuntimeInitialCreateExecutionReceipt receipt);
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.RejectedAuthority, status);
|
|
Assert.Equal(default, receipt);
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.False(lifetime.TryGetInitialCreateResidence(canonical, out _));
|
|
|
|
// A retry with the now-retired token is a distinct, safe no-op -
|
|
// proving Abandon actually retired the residence rather than
|
|
// leaving it sitting fully current for a replay.
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedToken,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, inputs, out RuntimeInitialCreateExecutionReceipt retryReceipt));
|
|
Assert.Equal(default, retryReceipt);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnvelopePositionStageRequiringSetPositionYieldsResumesAndPublishesOnceAfterCompletion()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 20UL);
|
|
const uint guid = 0x7002B000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
Assert.False(lease.Placement.IsValid);
|
|
Assert.Equal(0u, canonical.FullCellId);
|
|
AttachDormantBody(lifetime, canonical);
|
|
|
|
// Cellless (CommittedCellId == canonical.FullCellId == 0) forces
|
|
// SetPosition regardless of contact/distance for the non-local
|
|
// classify branch - this same-Create's OWN Position stage will
|
|
// require a real placement and yield mid-envelope.
|
|
WorldSession.EntitySpawn sameCreate = Spawn(guid, 1, positionSequence: 2, positionX: 15f);
|
|
PhysicsSpawnData physics = sameCreate.Physics!.Value;
|
|
sameCreate = sameCreate with
|
|
{
|
|
Physics = physics with
|
|
{
|
|
Timestamps = physics.Timestamps with { ObjDesc = 2, State = 2, Vector = 2 },
|
|
},
|
|
};
|
|
_ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
using IDisposable subscription = lifetime.Events.Subscribe(
|
|
new EntityObserver(delta => observed.Add(delta.Change)));
|
|
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt pending);
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
|
|
Assert.Equal(default, pending);
|
|
// ObjDesc already committed (buffered) before Position began its
|
|
// own placement lifecycle - prove NOTHING has published yet, even
|
|
// though a stage before the yield point already ran.
|
|
Assert.Empty(observed);
|
|
|
|
RuntimeEntityKey key = canonical.Key!.Value;
|
|
Assert.True(lifetime.InitialCreateExecution
|
|
.TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPosition, route.Disposition);
|
|
CompletePendingContinuationPlacement(lifetime, key, route);
|
|
Assert.Empty(observed);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
// Every buffered per-stage event from this ONE envelope publishes
|
|
// exactly once, only now that the envelope fully committed.
|
|
Assert.Equal(
|
|
[
|
|
RuntimeEntityChange.Updated,
|
|
RuntimeEntityChange.Updated,
|
|
RuntimeEntityChange.Updated,
|
|
RuntimeEntityChange.Updated,
|
|
RuntimeEntityChange.Updated,
|
|
],
|
|
observed);
|
|
RuntimeInitialCreateExecutedActionKind[] envelopeStages = receipt.Trace
|
|
.Where(static a => a.Sequence == 1UL)
|
|
.Select(static a => a.Kind)
|
|
.ToArray();
|
|
Assert.Equal(
|
|
[
|
|
RuntimeInitialCreateExecutedActionKind.PreTailDescriptionAdaptation,
|
|
RuntimeInitialCreateExecutedActionKind.ObjDesc,
|
|
RuntimeInitialCreateExecutedActionKind.Position,
|
|
RuntimeInitialCreateExecutedActionKind.State,
|
|
RuntimeInitialCreateExecutedActionKind.Vector,
|
|
RuntimeInitialCreateExecutedActionKind.WeenieDescription,
|
|
RuntimeInitialCreateExecutedActionKind.ResidentCellCleanup,
|
|
],
|
|
envelopeStages);
|
|
RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.SetPosition,
|
|
positionAction.PositionDisposition);
|
|
// The continuation's own SetPosition committed a real cell -
|
|
// ResidentCellCleanup now sees a claimed, resident entity.
|
|
Assert.Equal(Cell, canonical.FullCellId);
|
|
RuntimeInitialCreateExecutedAction cleanup = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ResidentCellCleanup);
|
|
Assert.Equal(
|
|
RuntimeResidentCellCleanupDisposition.ResidentUnmarked,
|
|
cleanup.ResidentCellCleanupDisposition);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
}
|
|
|
|
// Gap 2 (failure injection between every same-Create envelope stage):
|
|
// the Position stage is the ONLY boundary inside a SameIncarnationCreate
|
|
// envelope that this suite can interrupt via a REAL yield. Every other
|
|
// stage kind - PreTailDescriptionAdaptation, ObjDesc, CreateParent/
|
|
// Parent/Pickup, Movement, State, Vector, WeenieDescription,
|
|
// ResidentCellCleanup - is a pure, synchronous, in-memory
|
|
// RuntimeEntityRecord/InboundPhysicsStateController mutation with NO
|
|
// async placement lifecycle and NO event dispatched until the whole
|
|
// envelope's buffered publish flush at the very end (see ApplyEnvelope):
|
|
// there is no callback, no I/O, and no point where control ever returns
|
|
// to a caller (or where an event observer could reenter) mid-stage.
|
|
// Injecting a synthetic "failure" between two purely synchronous stages
|
|
// would require adding a diagnostic seam to production code purely to
|
|
// make a test possible, which this task's own instructions forbid. The
|
|
// Position stage is structurally different: it is the one action kind
|
|
// whose disposition can require a real RuntimeSetPositionState
|
|
// placement round-trip (Begin/Watch/yield/host-prepare-submit-
|
|
// acknowledge/resume) - the SAME mechanism a standalone (non-envelope)
|
|
// Position continuation uses. The two tests below exhaust that
|
|
// reachable boundary: a successful resume (see
|
|
// EnvelopePositionStageRequiringSetPositionYieldsResumesAndPublishesOnceAfterCompletion
|
|
// above) and an abandonment at that exact boundary (immediately below).
|
|
|
|
[Fact]
|
|
public void EnvelopeAbandonedDuringPositionStageYieldPublishesNothingAndConvergesEveryLedger()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 21UL);
|
|
const uint guid = 0x7002B100u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
Assert.False(lease.Placement.IsValid);
|
|
AttachDormantBody(lifetime, canonical);
|
|
|
|
// Same recipe as the successful-resume test: cellless forces
|
|
// SetPosition, guaranteeing a mid-envelope yield after ObjDesc (and
|
|
// Position's own snapshot-merge commit) already ran.
|
|
WorldSession.EntitySpawn sameCreate = Spawn(guid, 1, positionSequence: 2, positionX: 15f);
|
|
PhysicsSpawnData physics = sameCreate.Physics!.Value;
|
|
sameCreate = sameCreate with
|
|
{
|
|
Physics = physics with
|
|
{
|
|
Timestamps = physics.Timestamps with { ObjDesc = 2, State = 2, Vector = 2 },
|
|
},
|
|
};
|
|
_ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
using IDisposable subscription = lifetime.Events.Subscribe(
|
|
new EntityObserver(delta => observed.Add(delta.Change)));
|
|
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out _);
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
|
|
Assert.Empty(observed);
|
|
|
|
// Abandon between Execute retries at the yield: delete the entity
|
|
// rather than ever completing the continuation's placement.
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(guid, canonical.Incarnation),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: true,
|
|
out RuntimeEntityDeleteAcceptance acceptance));
|
|
lifetime.CompleteAcceptedDelete(acceptance);
|
|
|
|
// The delete's own Deleted is the only observed event - NONE of the
|
|
// envelope's buffered stages (ObjDesc, and Position's own snapshot
|
|
// merge, both of which had ALREADY committed before the yield) ever
|
|
// publish. TryAcceptDelete's own unconditional Physics.SetPosition.Forget
|
|
// already cancels the still-pending continuation placement here
|
|
// (same key, any token); DiscardProgress ALSO forgets it
|
|
// defensively (see its remarks) for callers that retire a
|
|
// residence without going through the full delete path.
|
|
Assert.Equal([RuntimeEntityChange.Deleted], observed);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
Assert.Equal(
|
|
0,
|
|
lifetime.Physics.SetPosition.CaptureOwnership().AcknowledgedPlacementCompletionCount);
|
|
|
|
// A stale Execute() with the original token, after the entity is
|
|
// gone, must not resurrect anything either.
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedToken,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
|
|
Assert.Equal(default, receipt);
|
|
}
|
|
|
|
[Fact]
|
|
public void EnvelopeStageRetryDoesNotDuplicateAlreadyCommittedStages()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 4UL);
|
|
const uint guid = 0x70022100u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
WorldSession.EntitySpawn sameCreate = Spawn(
|
|
guid, 1, includePosition: false, positionSequence: 2);
|
|
PhysicsSpawnData physics = sameCreate.Physics!.Value;
|
|
sameCreate = sameCreate with
|
|
{
|
|
Physics = physics with
|
|
{
|
|
Timestamps = physics.Timestamps with { Vector = 2 },
|
|
},
|
|
};
|
|
_ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
|
|
|
|
// The first Execute call fully drains the envelope in one synchronous
|
|
// pass (no yield point exists in this envelope). A SECOND call with
|
|
// the same (now-stale, released) token must not resurrect or
|
|
// reapply anything.
|
|
RuntimeInitialCreateExecutionReceipt first = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
ulong vectorAuthorityAfterFirst = canonical.VectorAuthorityVersion;
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedToken,
|
|
lifetime.InitialCreateExecution.Execute(canonical, lease.Token, NoContact, out _));
|
|
Assert.Equal(vectorAuthorityAfterFirst, canonical.VectorAuthorityVersion);
|
|
Assert.NotEmpty(first.Trace);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// D. Position routes
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void LocalOrdinaryPositionInterpolatesWithoutWorldPlacement()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 5UL);
|
|
const uint guid = 0x70023000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 15f);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: true, null, null, false, null,
|
|
out PositionTimestampDisposition disposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
|
|
var inputs = new RuntimeInitialCreateExecutionInputs(
|
|
UsePositionFromServer: true, PlayerDistance: 0f);
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, inputs);
|
|
|
|
RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.Interpolate,
|
|
positionAction.PositionDisposition);
|
|
Assert.Equal(15f, canonical.Snapshot.Position!.Value.PositionX);
|
|
// Interpolate never runs a physics placement: the cell stays exactly
|
|
// what the initial SetPosition already committed.
|
|
Assert.Equal(Cell, canonical.FullCellId);
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
// Round 4 R4-8: local ordinary route trace flags - CONSTRAIN-BEFORE
|
|
// (retail-notes.md function 3 line ~93041: ConstrainTo runs
|
|
// unconditionally BEFORE InterpolateTo on this route), unparent
|
|
// always runs on a received Position, and no teleport hook.
|
|
Assert.Equal(RuntimePositionConstrainPhase.BeforePositionOperation, positionAction.ConstrainPhase);
|
|
Assert.True(positionAction.UnparentBeforeRouting);
|
|
Assert.Equal(RuntimeTeleportHookPhase.None, positionAction.HookPhase);
|
|
}
|
|
|
|
[Fact]
|
|
public void LocalTeleportContinuationDrivesItsOwnAuthoredPlacementLifecycle()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 6UL);
|
|
const uint guid = 0x70023100u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 1, forcePositionSequence: 0, positionX: 40f);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: true, null, null, true, null,
|
|
out PositionTimestampDisposition disposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt pending);
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
|
|
Assert.Equal(default, pending);
|
|
// The snapshot's own position field is refreshed even before the
|
|
// physics placement resolves; the derived cell residency is not.
|
|
Assert.Equal(40f, canonical.Snapshot.Position!.Value.PositionX);
|
|
Assert.Equal(Cell, canonical.FullCellId);
|
|
|
|
RuntimeEntityKey key = canonical.Key!.Value;
|
|
Assert.True(lifetime.InitialCreateExecution
|
|
.TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, route.Disposition);
|
|
CompletePendingContinuationPlacement(lifetime, key, route);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
|
|
positionAction.PositionDisposition);
|
|
// Round 4 R4-8: local teleport route trace flags - ZeroVelocity,
|
|
// CONSTRAIN-AFTER (retail: TeleportPlayer runs first, ConstrainTo
|
|
// second), the AfterPositionOperation teleport hook, and unparent
|
|
// (a received Position always unsets parent).
|
|
Assert.True(positionAction.ZeroVelocity);
|
|
Assert.Equal(RuntimePositionConstrainPhase.AfterPositionOperation, positionAction.ConstrainPhase);
|
|
Assert.Equal(RuntimeTeleportHookPhase.AfterPositionOperation, positionAction.HookPhase);
|
|
Assert.True(positionAction.UnparentBeforeRouting);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoteNearContactPositionInterpolatesAfterInitialResidency()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 7UL);
|
|
const uint guid = 0x70023200u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 25f, isGrounded: true);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: false, null, null, false, null,
|
|
out PositionTimestampDisposition disposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
|
|
var inputs = new RuntimeInitialCreateExecutionInputs(
|
|
UsePositionFromServer: false, PlayerDistance: 10f);
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, inputs);
|
|
|
|
RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.Interpolate,
|
|
positionAction.PositionDisposition);
|
|
Assert.Equal(25f, canonical.Snapshot.Position!.Value.PositionX);
|
|
Assert.Equal(Cell, canonical.FullCellId);
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
// Round 4 R4-8: remote near route trace flag - constrain-after
|
|
// (remote route: MoveOrTeleport runs first, ConstrainTo second).
|
|
Assert.Equal(RuntimePositionConstrainPhase.AfterPositionOperation, positionAction.ConstrainPhase);
|
|
}
|
|
|
|
// Round 4 R4-7: wire IsGrounded=true disagrees with the body's OWN
|
|
// contact bit (forced false) - the local ordinary interpolate gate must
|
|
// follow the WIRE fact, not the body, per Round 3 A3.
|
|
[Fact]
|
|
public void LocalOrdinaryPositionRouteFollowsWireGroundedTrueWhenBodyContactIsFalse()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 97UL);
|
|
const uint guid = 0x70039000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
// Force the body's OWN contact bit to DISAGREE with the wire fact
|
|
// below - the route must ignore this.
|
|
ForceContact(canonical, inContact: false);
|
|
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 15f,
|
|
isGrounded: true);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: true, null, null, false, null,
|
|
out PositionTimestampDisposition disposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
|
|
var inputs = new RuntimeInitialCreateExecutionInputs(
|
|
UsePositionFromServer: true, PlayerDistance: 0f);
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, inputs);
|
|
|
|
RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.Interpolate,
|
|
positionAction.PositionDisposition);
|
|
}
|
|
|
|
// Round 4 R4-7: wire IsGrounded=false disagrees with the body's OWN
|
|
// contact bit (forced true) - the remote effective-contact gate must
|
|
// follow the WIRE fact, not the body.
|
|
[Fact]
|
|
public void RemotePositionRouteFollowsWireGroundedFalseWhenBodyContactIsTrue()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 98UL);
|
|
const uint guid = 0x70039100u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
// Force the body's OWN contact bit to DISAGREE with the wire fact
|
|
// below - the route must ignore this.
|
|
ForceContact(canonical, inContact: true);
|
|
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 25f,
|
|
isGrounded: false);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: false, null, null, false, null,
|
|
out PositionTimestampDisposition disposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
|
|
var inputs = new RuntimeInitialCreateExecutionInputs(
|
|
UsePositionFromServer: false, PlayerDistance: 10f);
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, inputs);
|
|
|
|
RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.NoPositionOperation,
|
|
positionAction.PositionDisposition);
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void RemoteFarPositionStopsInterpolatingAndRunsSetPositionSimple()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 8UL);
|
|
const uint guid = 0x70023300u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 25f);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: false, null, null, false, null,
|
|
out PositionTimestampDisposition disposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
|
|
var inputs = new RuntimeInitialCreateExecutionInputs(
|
|
UsePositionFromServer: false, PlayerDistance: 200f);
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, inputs, out _);
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
|
|
|
|
RuntimeEntityKey key = canonical.Key!.Value;
|
|
Assert.True(lifetime.InitialCreateExecution
|
|
.TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, route.Disposition);
|
|
Assert.True(route.StopInterpolating);
|
|
CompletePendingContinuationPlacement(lifetime, key, route);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, inputs);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
// Round 4 R4-8: remote far route trace flags - StopInterpolating
|
|
// and constrain-after.
|
|
Assert.True(positionAction.StopInterpolating);
|
|
Assert.Equal(RuntimePositionConstrainPhase.AfterPositionOperation, positionAction.ConstrainPhase);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParentedInitialCreateNeverRunsAWorldPlacementThroughTheExecutor()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 9UL);
|
|
const uint parentGuid = 0x70024100u;
|
|
const uint guid = 0x70024000u;
|
|
_ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
Assert.False(lease.Placement.IsValid);
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.AwaitFreshPosition, lease.Route.Disposition);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Equal(0u, receipt.FullCellId);
|
|
Assert.Equal(RuntimeTeleportHookPhase.None, receipt.TeleportHookPhase);
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ForcePositionContinuationRecordsSetPositionSimpleWithPreservedHeadingAndNoParentClear()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 45UL);
|
|
const uint guid = 0x7002F700u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
// Round 4 R4-12: give the entity a parent attachment BEFORE the
|
|
// ForcePosition update arrives, to structurally pin the merge
|
|
// body's deliberate combined shape - retail's FORCE_POSITION Gate A
|
|
// returns before CPhysicsObj::unset_parent ever runs, so the
|
|
// parent attachment must survive alongside the newly-applied
|
|
// Position after this drain.
|
|
const uint parentGuid = 0x7002F701u;
|
|
const uint parentLocation = 3u;
|
|
Assert.True(lifetime.Entities.TryCommitParent(
|
|
guid, parentGuid, parentLocation, placementId: 0u, positionSequence: 1,
|
|
out WorldSession.EntitySpawn parented));
|
|
lifetime.Entities.RefreshSnapshot(canonical, parented);
|
|
|
|
// isLocalPlayer + a fresh FORCE_POSITION_TS (1, newer than the
|
|
// seeded 0) + an EQUAL teleport sequence (0 == gate's current 0)
|
|
// blips immediately per PhysicsTimestampGate.TryAcceptPositionEvent.
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 1, positionX: 40f);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: true, null, null, false, null,
|
|
out PositionTimestampDisposition disposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
|
|
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out _);
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
|
|
RuntimeEntityKey key = canonical.Key!.Value;
|
|
Assert.True(lifetime.InitialCreateExecution
|
|
.TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, route.Disposition);
|
|
CompletePendingContinuationPlacement(lifetime, key, route);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
|
|
positionAction.PositionDisposition);
|
|
Assert.True(positionAction.PreserveHeading);
|
|
Assert.True(positionAction.SendPositionImmediately);
|
|
Assert.False(positionAction.StopInterpolating);
|
|
Assert.False(positionAction.ZeroVelocity);
|
|
Assert.Equal(RuntimePositionConstrainPhase.None, positionAction.ConstrainPhase);
|
|
// The FORCE_POSITION branch precedes unset_parent in retail's
|
|
// MoveOrTeleport - no parent clearing here.
|
|
Assert.False(positionAction.UnparentBeforeRouting);
|
|
// Round 4 R4-12: structurally pin the deliberate combined shape -
|
|
// Position AND the pre-existing Parent attachment are BOTH
|
|
// non-null after this merge.
|
|
Assert.NotNull(canonical.Snapshot.Position);
|
|
Assert.Equal(40f, canonical.Snapshot.Position!.Value.PositionX);
|
|
Assert.Equal(parentGuid, canonical.Snapshot.ParentGuid);
|
|
Assert.Equal(parentLocation, canonical.Snapshot.ParentLocation);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissileFlaggedEntityPositionContinuationClassifiesToProjectileAuthoritativeOperationKind()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 46UL);
|
|
const uint guid = 0x7002F800u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, missile: true),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
Assert.Equal(
|
|
RuntimeSetPositionOperationKind.ProjectileAuthoritative,
|
|
lease.Route.OperationKind);
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
// Already resident + far distance forces SetPositionSimple (the
|
|
// same "remote-shaped" branch a Projectile entity kind shares with
|
|
// Remote - the classifier has no Projectile-specific branch, only a
|
|
// Projectile-specific OperationKind mapping).
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 25f);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: false, null, null, false, null,
|
|
out PositionTimestampDisposition disposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
|
|
var inputs = new RuntimeInitialCreateExecutionInputs(
|
|
UsePositionFromServer: false, PlayerDistance: 200f);
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, inputs, out _);
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
|
|
|
|
RuntimeEntityKey key = canonical.Key!.Value;
|
|
Assert.True(lifetime.InitialCreateExecution
|
|
.TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, route.Disposition);
|
|
Assert.Equal(RuntimeSetPositionOperationKind.ProjectileAuthoritative, route.OperationKind);
|
|
Assert.True(route.StopInterpolating);
|
|
CompletePendingContinuationPlacement(lifetime, key, route);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, inputs);
|
|
Assert.NotEmpty(receipt.Trace);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
// Round 4 R4-8: projectile route trace flags - same StopInterpolating
|
|
// + constrain-after shape as the remote-far branch (the classifier
|
|
// has no Projectile-specific branch, only an OperationKind mapping).
|
|
RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
Assert.True(positionAction.StopInterpolating);
|
|
Assert.Equal(RuntimePositionConstrainPhase.AfterPositionOperation, positionAction.ConstrainPhase);
|
|
}
|
|
|
|
// Contrasts with ParentedInitialCreateNeverRunsAWorldPlacementThroughTheExecutor:
|
|
// the PickedUp residence kind (no parent AND no position) is the OTHER
|
|
// branch that classifies to AwaitFreshPosition at Begin/Create time (see
|
|
// RuntimeInitialCreateResidenceState.Begin's residence-kind switch).
|
|
// NOTE (mandated-case coverage gap): a standalone Position CONTINUATION
|
|
// (as opposed to this initial-lease route) can never itself classify to
|
|
// AwaitFreshPosition - RuntimeAuthoritativePositionRouteClassifier.
|
|
// ClassifyAcceptedPosition (the only classifier ApplyPositionAction ever
|
|
// calls) has no branch that returns AwaitFreshPosition; that disposition
|
|
// is produced exclusively by ClassifyCreate (Parented/PickedUp) and
|
|
// ClassifyLeaveWorld (neither of which ApplyPositionAction calls). A
|
|
// parented/picked entity's raw Position wire events are retained as
|
|
// Position continuations exactly like any other entity's and are
|
|
// classified with the SAME Remote/LocalPlayer logic once drained - the
|
|
// "picked" or "parented" fact plays no role in that classification. This
|
|
// is a structural property of the current classifier, not a gap in this
|
|
// test suite; the reachable form of "AwaitFreshPosition for a
|
|
// parented/picked entity" is the INITIAL LEASE route exercised here and
|
|
// by ParentedInitialCreateNeverRunsAWorldPlacementThroughTheExecutor.
|
|
[Fact]
|
|
public void PickedUpInitialCreateNeverRunsAWorldPlacementThroughTheExecutor()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 47UL);
|
|
const uint guid = 0x7002F900u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
Assert.False(lease.Placement.IsValid);
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.AwaitFreshPosition, lease.Route.Disposition);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Equal(0u, receipt.FullCellId);
|
|
Assert.Equal(RuntimeTeleportHookPhase.None, receipt.TeleportHookPhase);
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// Round 3 B10 (execution-time-rejected retained Position): admission
|
|
// ACCEPTED (disposition Apply - POSITION_TS genuinely advanced), but
|
|
// execution-time classification REJECTS on a nonfinite derived distance.
|
|
// The gate-consumed timestamps must still land in the snapshot; no pose
|
|
// ever applies.
|
|
[Fact]
|
|
public void ExecutionTimeRejectedPositionStampsRetainedTimestampsWithoutPoseApplication()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 48UL);
|
|
const uint guid = 0x7002FA00u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
Assert.Equal(Cell, canonical.FullCellId);
|
|
float originalPositionX = canonical.Snapshot.Position!.Value.PositionX;
|
|
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 25f);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: false, null, null, false, null,
|
|
out PositionTimestampDisposition disposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
|
|
var inputs = new RuntimeInitialCreateExecutionInputs(
|
|
UsePositionFromServer: false, PlayerDistance: float.NaN);
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, inputs);
|
|
|
|
RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.RejectedData,
|
|
positionAction.PositionDisposition);
|
|
// The retained Position/Teleport/ForcePosition channels the gate
|
|
// actually moved land in the snapshot ...
|
|
Assert.Equal(2, canonical.Snapshot.Physics!.Value.Timestamps.Position);
|
|
Assert.Equal(0, canonical.Snapshot.Physics!.Value.Timestamps.Teleport);
|
|
Assert.Equal(0, canonical.Snapshot.Physics!.Value.Timestamps.ForcePosition);
|
|
// ... but no pose ever applies.
|
|
Assert.Equal(originalPositionX, canonical.Snapshot.Position!.Value.PositionX);
|
|
Assert.Equal(Cell, canonical.FullCellId);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// E. Missing-parent raw deferred-create replay
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void MissingParentReplayConsumesExactAdmissionIdAndRegistersChildThroughCanonicalRoute()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 10UL);
|
|
const uint parentGuid = 0x70025100u;
|
|
const uint childGuid = 0x70025000u;
|
|
RuntimeEntityRegistrationResult deferred = lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false);
|
|
Assert.True(deferred.DeferredForParent);
|
|
Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent,
|
|
out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, parent, parentLease.Token, NoContact);
|
|
|
|
Assert.Equal(1, receipt.ReplayedDeferredChildCount);
|
|
RuntimeInitialCreateExecutedAction replay = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.DeferredChildReplay);
|
|
Assert.Equal(RuntimeDeferredChildReplayOutcome.Registered, replay.DeferredChildOutcome);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord child));
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(child, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void StaleAdmissionIdCannotConsumeAReplacementQueuedAfterThePeek()
|
|
{
|
|
var parents = new ParentAttachmentState();
|
|
const uint parentGuid = 0x70025200u;
|
|
WorldSession.EntitySpawn spawn = Spawn(
|
|
0x70025300u, 1, includePosition: false, parentGuid: parentGuid);
|
|
parents.EnqueueDeferredCreate(spawn, isLocalPlayer: false);
|
|
Assert.True(parents.TryPeekDeferredCreate(parentGuid, out DeferredParentCreate stale));
|
|
|
|
parents.Clear();
|
|
parents.EnqueueDeferredCreate(spawn, isLocalPlayer: false);
|
|
Assert.True(parents.TryPeekDeferredCreate(parentGuid, out DeferredParentCreate replacement));
|
|
Assert.NotEqual(stale.AdmissionId, replacement.AdmissionId);
|
|
|
|
Assert.False(parents.ConsumeDeferredCreate(parentGuid, stale));
|
|
Assert.Equal(1, parents.DeferredCreateCount);
|
|
Assert.True(parents.ConsumeDeferredCreate(parentGuid, replacement));
|
|
Assert.Equal(0, parents.DeferredCreateCount);
|
|
}
|
|
|
|
// Round 3 B7: multiple children queued behind the SAME missing parent
|
|
// all replay atomically through the executor's whole-bucket detach
|
|
// (ParentAttachmentState.DetachDeferredCreates), in FIFO admission
|
|
// order, exercised through the canonical RunInitialTail path rather than
|
|
// against ParentAttachmentState directly.
|
|
[Fact]
|
|
public void MultipleChildrenQueuedBehindOneMissingParentReplayInFifoOrderThroughTheExecutor()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 32UL);
|
|
const uint parentGuid = 0x7002E100u;
|
|
const uint firstChildGuid = 0x7002E000u;
|
|
const uint secondChildGuid = 0x7002E001u;
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(firstChildGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(secondChildGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.Equal(2, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, parent, parentLease.Token, NoContact);
|
|
|
|
Assert.Equal(2, receipt.ReplayedDeferredChildCount);
|
|
RuntimeInitialCreateExecutedAction[] replays = receipt.Trace
|
|
.Where(static a => a.Kind is RuntimeInitialCreateExecutedActionKind.DeferredChildReplay)
|
|
.ToArray();
|
|
Assert.Equal(2, replays.Length);
|
|
Assert.All(
|
|
replays,
|
|
static a => Assert.Equal(RuntimeDeferredChildReplayOutcome.Registered, a.DeferredChildOutcome));
|
|
Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
Assert.True(lifetime.Entities.TryGetActive(firstChildGuid, out _));
|
|
Assert.True(lifetime.Entities.TryGetActive(secondChildGuid, out _));
|
|
}
|
|
|
|
// Round 4 R4-1: one child's registration THROWING must not strand the
|
|
// remaining siblings or escape Execute as an exception. Three children
|
|
// queued behind one missing parent; the SECOND child's own registration
|
|
// callback is made to throw (via a reflection-swapped
|
|
// _registerDeferredChild delegate - the standard fault-injection
|
|
// technique this file already uses for private-state pokes, e.g.
|
|
// SetCompletedAdoptionRevision). The third child must still register.
|
|
[Fact]
|
|
public void DeferredChildReplayContainsOneChildsThrowingRegistrationAndContinuesWithSiblings()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 92UL);
|
|
const uint parentGuid = 0x70035000u;
|
|
const uint firstChildGuid = 0x70035001u;
|
|
const uint secondChildGuid = 0x70035002u;
|
|
const uint thirdChildGuid = 0x70035003u;
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(firstChildGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(secondChildGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(thirdChildGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.Equal(3, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
|
|
WrapDeferredChildRegistration(lifetime, (original, spawn, isLocalPlayer) =>
|
|
{
|
|
if (spawn.Guid == secondChildGuid)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Injected R4-1 containment test failure.");
|
|
}
|
|
return original(spawn, isLocalPlayer);
|
|
});
|
|
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
// No exception escapes Execute - it returns a typed status.
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, parent, parentLease.Token, NoContact);
|
|
|
|
Assert.Equal(3, receipt.ReplayedDeferredChildCount);
|
|
RuntimeInitialCreateExecutedAction[] replays = receipt.Trace
|
|
.Where(static a => a.Kind is RuntimeInitialCreateExecutedActionKind.DeferredChildReplay)
|
|
.ToArray();
|
|
Assert.Equal(3, replays.Length);
|
|
Assert.Equal(RuntimeDeferredChildReplayOutcome.Registered, replays[0].DeferredChildOutcome);
|
|
Assert.Equal(RuntimeDeferredChildReplayOutcome.Rejected, replays[1].DeferredChildOutcome);
|
|
Assert.Equal(RuntimeDeferredChildReplayOutcome.Registered, replays[2].DeferredChildOutcome);
|
|
Assert.True(lifetime.Entities.TryGetActive(firstChildGuid, out _));
|
|
Assert.False(lifetime.Entities.TryGetActive(secondChildGuid, out _));
|
|
Assert.True(lifetime.Entities.TryGetActive(thirdChildGuid, out _));
|
|
Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
// Round 5 R5-3: the contained exception is recorded on the
|
|
// observable failure surface, not silently swallowed.
|
|
Assert.Equal(1, lifetime.CaptureOwnership().ReplayFailureCount);
|
|
Assert.True(lifetime.CaptureOwnership().HasLastReplayFailure);
|
|
}
|
|
|
|
// Round 4 R4-1: a mid-loop abandonment (the parent entity is no longer
|
|
// current) must RESTORE the unprocessed remainder rather than
|
|
// permanently destroy it. Two children queued behind one missing
|
|
// parent; the FIRST child's registration callback reentrantly deletes
|
|
// the PARENT (simulating a synchronous observer reaction fired from
|
|
// within registration). The second child's raw Create must be back in
|
|
// the deferred bucket afterward (observable via ContainsDeferredCreate),
|
|
// and every ledger must still converge.
|
|
[Fact]
|
|
public void DeferredChildReplayRestoresTheUnprocessedRemainderWhenTheParentIsDeletedReentrantlyMidReplay()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 93UL);
|
|
const uint parentGuid = 0x70036000u;
|
|
const uint firstChildGuid = 0x70036001u;
|
|
const uint secondChildGuid = 0x70036002u;
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(firstChildGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(secondChildGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.Equal(2, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
|
|
WrapDeferredChildRegistration(lifetime, (original, spawn, isLocalPlayer) =>
|
|
{
|
|
RuntimeEntityRegistrationResult result = original(spawn, isLocalPlayer);
|
|
if (spawn.Guid == firstChildGuid)
|
|
{
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(parentGuid, 1),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: true,
|
|
out RuntimeEntityDeleteAcceptance acceptance));
|
|
lifetime.CompleteAcceptedDelete(acceptance);
|
|
}
|
|
return result;
|
|
});
|
|
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedAuthority,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
|
|
Assert.Equal(default, receipt);
|
|
|
|
// Child 1 legitimately registered before the reentrant delete
|
|
// fired; child 2's raw Create is restored to the deferred bucket
|
|
// rather than lost.
|
|
Assert.True(lifetime.Entities.TryGetActive(firstChildGuid, out RuntimeEntityRecord firstChild));
|
|
Assert.True(lifetime.Entities.ParentAttachments.ContainsDeferredCreate(secondChildGuid, 1));
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
// Only the PARENT's own residence/progress was touched by this
|
|
// abandoned Execute call (matching the existing
|
|
// DeleteDuringDeferredChildReplayAbandonsExecutionWithoutResurrection
|
|
// precedent) - child 1 is a distinct entity the deferred replay
|
|
// legitimately registered through the canonical route before the
|
|
// reentrant delete happened; its own (never executed) residence
|
|
// lease correctly remains open, accounting for the sole surviving
|
|
// lease count.
|
|
Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(firstChild, out _));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Round 4 R4-1 test-only fault injection: swaps the executor's private
|
|
/// <c>_registerDeferredChild</c> delegate for one that wraps the
|
|
/// original, matching this file's existing reflection-based
|
|
/// private-state pokes (e.g. SetCompletedAdoptionRevision below).
|
|
/// </summary>
|
|
private static void WrapDeferredChildRegistration(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
Func<
|
|
Func<WorldSession.EntitySpawn, bool, RuntimeEntityRegistrationResult>,
|
|
WorldSession.EntitySpawn,
|
|
bool,
|
|
RuntimeEntityRegistrationResult> wrapper)
|
|
{
|
|
Type executorType = typeof(RuntimeInitialCreateContinuationExecutor);
|
|
System.Reflection.FieldInfo field = executorType.GetField(
|
|
"_registerDeferredChild",
|
|
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;
|
|
var original = (Func<WorldSession.EntitySpawn, bool, RuntimeEntityRegistrationResult>)
|
|
field.GetValue(lifetime.InitialCreateExecution)!;
|
|
Func<WorldSession.EntitySpawn, bool, RuntimeEntityRegistrationResult> wrapped =
|
|
(spawn, isLocalPlayer) => wrapper(original, spawn, isLocalPlayer);
|
|
field.SetValue(lifetime.InitialCreateExecution, wrapped);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Round 5 R5-1 test helper: both <c>RuntimeEntityObjectLifetime.
|
|
/// TryApplyParent</c> (line ~908) and <c>RegisterEntityCore</c>'s own
|
|
/// missing-parent gate divert an UNADDRESSABLE-at-admission parent
|
|
/// relation to a DIFFERENT mechanism entirely (the legacy unresolved-
|
|
/// wait queue, or a raw deferred Create) - neither ever reaches the
|
|
/// residence continuation FIFO this round's executor dispatch code
|
|
/// operates on. The scenarios this round's tests exercise (drain-time/
|
|
/// replay-time staleness) all require the parent to be ADDRESSABLE at
|
|
/// the moment of admission and only become stale/unaddressable
|
|
/// afterward - exactly the shape the original Round 3/4 test used.
|
|
/// This helper registers a real parent at
|
|
/// <paramref name="namedParentIncarnation"/>, admits the standalone
|
|
/// Parent continuation naming it, then removes the parent from the
|
|
/// ACTIVE directory directly (<c>RuntimeEntityDirectory.RemoveActive</c>)
|
|
/// rather than through the full DeleteObject-acceptance ceremony - a
|
|
/// real delete retains a permanent per-(guid,generation) teardown
|
|
/// tombstone (<c>RuntimeEntityDirectory.RetainTeardown</c>), which would
|
|
/// collide if a later test step re-registers the SAME guid at the SAME
|
|
/// incarnation (an invalid combination this scenario has no reason to
|
|
/// exercise - retail incarnations only ever increase). RemoveActive
|
|
/// makes the parent unaddressable for <c>TryGetActive</c> purposes
|
|
/// without that permanent marker, while the gate stays intact so a
|
|
/// LATER same-incarnation re-registration takes the codebase's own
|
|
/// existing "recovered CreateObject" ExistingGeneration path.
|
|
/// </summary>
|
|
private static void AdmitThenOrphanParentRelation(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
uint parentGuid,
|
|
uint childGuid,
|
|
ushort namedParentIncarnation,
|
|
uint parentLocation = 1u)
|
|
{
|
|
// includePosition:false - besides needing no physics engine, this
|
|
// also matters for the "same incarnation returns" scenario: a
|
|
// position-bearing retained snapshot would carry over through
|
|
// MergeUntimestampedCreate's Position=retained.Position copy when
|
|
// the SAME (guid, incarnation) is later re-registered on the
|
|
// "recovered CreateObject" ExistingGeneration path, reclassifying
|
|
// the recovered residence as TopLevel (SetPosition) instead of
|
|
// PickedUp (AwaitFreshPosition) and stalling RunToCompletion on an
|
|
// unrequested placement ack.
|
|
RuntimeEntityRecord parent = lifetime.RegisterEntity(
|
|
Spawn(parentGuid, namedParentIncarnation, includePosition: false)).Canonical!;
|
|
var parentUpdate = new ParentEvent.Parsed(
|
|
parentGuid,
|
|
childGuid,
|
|
ParentLocation: parentLocation,
|
|
PlacementId: 0u,
|
|
ParentInstanceSequence: namedParentIncarnation,
|
|
ChildPositionSequence: 2);
|
|
Assert.True(lifetime.TryApplyParent(parentUpdate, null, out _));
|
|
Assert.True(lifetime.Entities.RemoveActive(parent));
|
|
}
|
|
|
|
// Round 3 B9 revalidated a standalone Parent continuation's parent
|
|
// incarnation at EXECUTION time, not just admission time - admission
|
|
// succeeds while the parent is still active; the parent is then
|
|
// deleted before the drain reaches this continuation. Round 4 R4-5
|
|
// pinned the mismatch outcome as a DISCARD; Round 5 R5-1 OVERTURNS
|
|
// that with hard retail evidence (QueueBlobForObject, pseudo-C 92326;
|
|
// GUID-keyed CObjectMaint bucket, 271082-271088) - retail QUEUES a
|
|
// relation whose parent is unaddressable under the PARENT's guid and
|
|
// replays it when that guid is created; it never discards on this
|
|
// path. This test now covers R5-1's mandated "parent-returns -> queued
|
|
// relation applies exactly once" scenario end-to-end.
|
|
[Fact]
|
|
public void ParentContinuationRevalidatesLiveParentAtExecutionAndQueuesOnMismatchThenAppliesWhenTheParentArrives()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 33UL);
|
|
const uint parentGuid = 0x7002E200u;
|
|
const uint childGuid = 0x7002E300u;
|
|
_ = lifetime.RegisterEntity(Spawn(parentGuid, 1));
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
var parentUpdate = new ParentEvent.Parsed(
|
|
parentGuid,
|
|
childGuid,
|
|
ParentLocation: 1u,
|
|
PlacementId: 0u,
|
|
ParentInstanceSequence: 1,
|
|
ChildPositionSequence: 2);
|
|
// Admission succeeds - the parent is still active at this moment.
|
|
Assert.True(lifetime.TryApplyParent(parentUpdate, null, out _));
|
|
|
|
// The parent is deleted BEFORE the drain ever reaches this
|
|
// continuation - admission-time validation cannot see this.
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(parentGuid, 1),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: true,
|
|
out RuntimeEntityDeleteAcceptance acceptance));
|
|
lifetime.CompleteAcceptedDelete(acceptance);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
// Queued (retail-faithful), not discarded: the residence still
|
|
// converges cleanly and the child's own canonical record survives.
|
|
RuntimeInitialCreateExecutedAction parentAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Parent);
|
|
Assert.Equal(
|
|
RuntimeParentRelationOutcome.DeferredAwaitingParent,
|
|
parentAction.ParentRelationOutcome);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.True(lifetime.Entities.TryGetActive(childGuid, out _));
|
|
Assert.True(lifetime.Entities.ParentAttachments
|
|
.ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
|
|
Assert.Null(canonical.Snapshot.ParentGuid);
|
|
}
|
|
|
|
// Round 5 R5-1 mandated: "parent-returns -> queued relation applies
|
|
// exactly once" (trace + attach), end-to-end. Uses
|
|
// AdmitThenOrphanParentRelation (RemoveActive, not a full
|
|
// DeleteObject-acceptance) specifically so the SAME (guid, incarnation)
|
|
// can validly reappear afterward through the codebase's own existing
|
|
// "recovered CreateObject" ExistingGeneration path, without an
|
|
// artificial teardown-tombstone collision the scenario has no reason
|
|
// to exercise.
|
|
[Fact]
|
|
public void DeferredAcceptedParentRelationAppliesExactlyOnceWhenTheSameParentIncarnationReturns()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 113UL);
|
|
const uint parentGuid = 0x70049000u;
|
|
const uint childGuid = 0x70049001u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
AdmitThenOrphanParentRelation(lifetime, parentGuid, childGuid, namedParentIncarnation: 1);
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
RuntimeInitialCreateExecutedAction parentAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Parent);
|
|
Assert.Equal(RuntimeParentRelationOutcome.DeferredAwaitingParent, parentAction.ParentRelationOutcome);
|
|
Assert.True(lifetime.Entities.ParentAttachments
|
|
.ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
|
|
|
|
// The SAME parent incarnation (1) reappears - the queued relation
|
|
// replays and applies exactly once.
|
|
RuntimeEntityRecord recreatedParent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
recreatedParent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
RuntimeInitialCreateExecutionReceipt parentReceipt = RunToCompletion(
|
|
lifetime, recreatedParent, parentLease.Token, NoContact);
|
|
|
|
RuntimeInitialCreateExecutedAction replayAction = Assert.Single(
|
|
parentReceipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ParentRelationReplay);
|
|
Assert.Equal(RuntimeParentRelationOutcome.Applied, replayAction.ParentRelationOutcome);
|
|
// Applied commits the SAME position-timestamp-only merge a live
|
|
// Parent continuation commits (already stamped at the relation's
|
|
// original drain, before it was queued) - it does not itself set
|
|
// ParentGuid/ParentLocation, matching
|
|
// StandaloneParentContinuationAppliesPositionTimestampOnlyMergeWithNoReDefer's
|
|
// established precedent that the actual attach commit is
|
|
// TryCommitParent's (App-layer EquippedChildRenderController's) job.
|
|
Assert.Equal(2, canonical.Snapshot.Physics!.Value.Timestamps.Position);
|
|
Assert.Null(canonical.Snapshot.ParentGuid);
|
|
Assert.False(lifetime.Entities.ParentAttachments
|
|
.ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
|
|
Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// Round 5 R5-1 mandated: the arriving parent's incarnation is NEWER
|
|
// than the one the relation named - discard per Resolve's own rule
|
|
// (the current live parent supersedes the packet).
|
|
[Fact]
|
|
public void DeferredAcceptedParentRelationDiscardsWhenTheArrivingParentIsANewerIncarnationThanTheRelationNamed()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 104UL);
|
|
const uint parentGuid = 0x7003F000u;
|
|
const uint childGuid = 0x7003F001u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
// Admits the standalone Parent continuation while the parent IS
|
|
// addressable (required - see AdmitThenOrphanParentRelation's
|
|
// remarks), then deletes the parent so the child's OWN drain
|
|
// discovers it stale and queues the relation.
|
|
AdmitThenOrphanParentRelation(lifetime, parentGuid, childGuid, namedParentIncarnation: 1);
|
|
_ = RunToCompletion(lifetime, canonical, lease.Token, NoContact);
|
|
Assert.True(lifetime.Entities.ParentAttachments
|
|
.ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
|
|
|
|
// Parent arrives directly at incarnation 2 - NEWER than the
|
|
// relation's named incarnation 1.
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 2, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, parent, parentLease.Token, NoContact);
|
|
|
|
RuntimeInitialCreateExecutedAction replayAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ParentRelationReplay);
|
|
Assert.Equal(RuntimeParentRelationOutcome.DiscardedStaleParent, replayAction.ParentRelationOutcome);
|
|
Assert.False(lifetime.Entities.ParentAttachments
|
|
.ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
|
|
Assert.Null(canonical.Snapshot.ParentGuid);
|
|
Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
|
|
}
|
|
|
|
// Round 5 R5-1 mandated: the arriving parent's incarnation is OLDER
|
|
// than the one the relation named - stays queued (wait), then applies
|
|
// on the NEXT matching incarnation. This scenario is not constructible
|
|
// through the normal admission ceremony: naming ParentInstanceSequence
|
|
// 5 in a Parent continuation requires the LIVE parent to already BE at
|
|
// instance 5 at admission time (TryApplyParent's own gate), which
|
|
// pins the parent's OWN PhysicsTimestampGate at 5 - a client can never
|
|
// subsequently see that SAME guid at an OLDER instance 3 afterward
|
|
// (retail incarnations are per-guid monotonic; the gate enforces it).
|
|
// This test instead enqueues the relation directly through
|
|
// ParentAttachmentState's own public API (the same technique the ABA
|
|
// test below uses) to isolate ApplyReplayedParentRelation's OWN
|
|
// incarnation-compare logic from that inapplicable precondition.
|
|
[Fact]
|
|
public void DeferredAcceptedParentRelationStaysQueuedWhenTheArrivingParentIsOlderThanTheRelationNamedAndAppliesOnTheNextMatchingIncarnation()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 105UL);
|
|
const uint parentGuid = 0x70040000u;
|
|
const uint childGuid = 0x70040001u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
_ = RunToCompletion(lifetime, canonical, lease.Token, NoContact);
|
|
var parentUpdate = new ParentEvent.Parsed(
|
|
parentGuid, childGuid, ParentLocation: 1u, PlacementId: 0u,
|
|
ParentInstanceSequence: 5, ChildPositionSequence: 2);
|
|
lifetime.Entities.ParentAttachments.EnqueueDeferredAcceptedRelation(
|
|
childGuid, canonical.Key!.Value, parentUpdate, null, default);
|
|
|
|
// Parent arrives at incarnation 3 - OLDER than the relation's
|
|
// named incarnation 5.
|
|
RuntimeEntityRecord parentAtThree = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 3, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parentAtThree, out RuntimeInitialCreateResidenceLease leaseThree));
|
|
RuntimeInitialCreateExecutionReceipt receiptThree = RunToCompletion(
|
|
lifetime, parentAtThree, leaseThree.Token, NoContact);
|
|
RuntimeInitialCreateExecutedAction replayThree = Assert.Single(
|
|
receiptThree.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ParentRelationReplay);
|
|
Assert.Equal(RuntimeParentRelationOutcome.DeferredAwaitingParent, replayThree.ParentRelationOutcome);
|
|
Assert.True(lifetime.Entities.ParentAttachments
|
|
.ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
|
|
|
|
// Parent recreated at incarnation 5 - now matches.
|
|
RuntimeEntityRecord parentAtFive = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 5, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parentAtFive, out RuntimeInitialCreateResidenceLease leaseFive));
|
|
RuntimeInitialCreateExecutionReceipt receiptFive = RunToCompletion(
|
|
lifetime, parentAtFive, leaseFive.Token, NoContact);
|
|
RuntimeInitialCreateExecutedAction replayFive = Assert.Single(
|
|
receiptFive.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ParentRelationReplay);
|
|
Assert.Equal(RuntimeParentRelationOutcome.Applied, replayFive.ParentRelationOutcome);
|
|
// Applied never sets ParentGuid itself - see
|
|
// StandaloneParentContinuationAppliesPositionTimestampOnlyMergeWithNoReDefer's
|
|
// established precedent (the actual attach commit is
|
|
// TryCommitParent's job, out of scope for this residence drain).
|
|
Assert.Null(canonical.Snapshot.ParentGuid);
|
|
Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
|
|
}
|
|
|
|
// Round 5 R5-1 mandated: the child is deleted while its relation is
|
|
// still queued (parent never showed up) - the entry is cancelled and
|
|
// every ledger converges.
|
|
[Fact]
|
|
public void DeferredAcceptedParentRelationIsCancelledWhenTheChildIsDeletedWhileQueued()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 106UL);
|
|
const uint parentGuid = 0x70041000u;
|
|
const uint childGuid = 0x70041001u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
AdmitThenOrphanParentRelation(lifetime, parentGuid, childGuid, namedParentIncarnation: 1);
|
|
_ = RunToCompletion(lifetime, canonical, lease.Token, NoContact);
|
|
Assert.Equal(1, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
|
|
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(childGuid, 1),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: true,
|
|
out RuntimeEntityDeleteAcceptance acceptance));
|
|
lifetime.CompleteAcceptedDelete(acceptance);
|
|
|
|
Assert.False(lifetime.Entities.ParentAttachments
|
|
.ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
|
|
Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
|
|
}
|
|
|
|
// Round 5 R5-1 mandated: a full session reset while a relation is
|
|
// queued clears it and converges.
|
|
[Fact]
|
|
public void DeferredAcceptedParentRelationClearsOnSessionResetAndConverges()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 107UL);
|
|
const uint parentGuid = 0x70042000u;
|
|
const uint childGuid = 0x70042001u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
AdmitThenOrphanParentRelation(lifetime, parentGuid, childGuid, namedParentIncarnation: 1);
|
|
_ = RunToCompletion(lifetime, canonical, lease.Token, NoContact);
|
|
Assert.Equal(1, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
|
|
|
|
IReadOnlyList<RuntimeEntityRecord> retirements = lifetime.BeginSessionClear();
|
|
Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
|
|
foreach (RuntimeEntityRecord record in retirements)
|
|
lifetime.CompleteSessionEntityRetirement(record);
|
|
Assert.True(lifetime.CompleteSessionClearIfConverged());
|
|
}
|
|
|
|
// Round 5 R5-1 mandated ABA coverage, tested directly against
|
|
// ParentAttachmentState (mirroring
|
|
// StaleAdmissionIdCannotConsumeAReplacementQueuedAfterThePeek's own
|
|
// direct-state style): a window token minted BEFORE a Clear() must not
|
|
// restore anything afterward, even though a NEW window for the same
|
|
// parent guid could otherwise reuse its Id space.
|
|
[Fact]
|
|
public void DeferredAcceptedParentRelationStaleWindowTokenCannotRestoreAfterClearAndRequeue()
|
|
{
|
|
var parents = new ParentAttachmentState();
|
|
const uint parentGuid = 0x70043000u;
|
|
const uint childGuid = 0x70043001u;
|
|
var childKey = new RuntimeEntityKey(1u, 1);
|
|
var parentUpdate = new ParentEvent.Parsed(
|
|
parentGuid, childGuid, ParentLocation: 1u, PlacementId: 0u,
|
|
ParentInstanceSequence: 1, ChildPositionSequence: 2);
|
|
parents.EnqueueDeferredAcceptedRelation(childGuid, childKey, parentUpdate, null, default);
|
|
|
|
ImmutableArray<DeferredAcceptedParentRelation> detached =
|
|
parents.DetachDeferredAcceptedRelations(parentGuid, out DeferredReplayWindowToken staleWindow);
|
|
DeferredAcceptedParentRelation stale = Assert.Single(detached);
|
|
|
|
// Reentrant Clear() (session reset) while the window is still open.
|
|
parents.Clear();
|
|
|
|
// A restore against the now-stale token must be a silent no-op.
|
|
parents.RestoreDeferredAcceptedRelations(staleWindow, [stale]);
|
|
Assert.Equal(0, parents.DeferredAcceptedRelationCount);
|
|
Assert.False(parents.ContainsDeferredAcceptedRelation(childGuid, childKey));
|
|
}
|
|
|
|
// Round 5 R5-1 mandated: the envelope's CreateParent stage, end-to-end,
|
|
// for the unaddressable-parent flavor (addressability-only, no
|
|
// incarnation to compare). RegisterEntityCore's OWN missing-parent gate
|
|
// (checked before PreviewCreateDisposition, for ANY beginInitialResidence
|
|
// call) diverts a same-incarnation update naming an UNADDRESSABLE parent
|
|
// to a raw deferred Create entirely, bypassing the envelope/CreateParent
|
|
// stage - so, exactly like the standalone Parent continuation, this
|
|
// scenario requires the parent to be ADDRESSABLE at the moment of
|
|
// admission and only orphaned afterward.
|
|
[Fact]
|
|
public void EnvelopeCreateParentQueuesForUnaddressableParentAndAppliesWhenTheParentArrives()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 108UL);
|
|
const uint parentGuid = 0x70044000u;
|
|
const uint childGuid = 0x70044001u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
// includePosition:false, matching AdmitThenOrphanParentRelation's own
|
|
// remarks - avoids both the physics-engine dependency and a
|
|
// position-bearing retained snapshot bleeding into the LATER
|
|
// same-incarnation "recovered CreateObject" merge below.
|
|
RuntimeEntityRecord orphanedParent = lifetime.RegisterEntity(
|
|
Spawn(parentGuid, 1, includePosition: false)).Canonical!;
|
|
|
|
WorldSession.EntitySpawn sameCreate = Spawn(
|
|
childGuid, 1, includePosition: false, positionSequence: 2, parentGuid: parentGuid);
|
|
PhysicsSpawnData physics = sameCreate.Physics!.Value;
|
|
sameCreate = sameCreate with
|
|
{
|
|
Physics = physics with
|
|
{
|
|
Timestamps = physics.Timestamps with { State = 2, Vector = 2 },
|
|
},
|
|
};
|
|
_ = lifetime.RegisterEntityWithInitialResidence(sameCreate, isLocalPlayer: false);
|
|
|
|
// The parent is now orphaned - removed from the active directory
|
|
// AFTER the envelope's CreateParent stage was admitted while it was
|
|
// still addressable. RemoveActive (not a full DeleteObject-acceptance)
|
|
// for the SAME reason AdmitThenOrphanParentRelation uses it: a real
|
|
// delete retains a permanent per-(guid,generation) teardown tombstone
|
|
// that would collide when the SAME incarnation is re-registered below.
|
|
Assert.True(lifetime.Entities.RemoveActive(orphanedParent));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
RuntimeInitialCreateExecutedAction createParentAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.CreateParent);
|
|
Assert.Equal(RuntimeParentRelationOutcome.DeferredAwaitingParent, createParentAction.ParentRelationOutcome);
|
|
Assert.True(lifetime.Entities.ParentAttachments
|
|
.ContainsDeferredAcceptedRelation(childGuid, canonical.Key!.Value));
|
|
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
RuntimeInitialCreateExecutionReceipt parentReceipt = RunToCompletion(
|
|
lifetime, parent, parentLease.Token, NoContact);
|
|
RuntimeInitialCreateExecutedAction replayAction = Assert.Single(
|
|
parentReceipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.ParentRelationReplay);
|
|
Assert.Equal(RuntimeParentRelationOutcome.Applied, replayAction.ParentRelationOutcome);
|
|
// Applied never sets ParentGuid itself - see
|
|
// StandaloneParentContinuationAppliesPositionTimestampOnlyMergeWithNoReDefer's
|
|
// established precedent (the actual attach commit is
|
|
// TryCommitParent's job, out of scope for this residence drain).
|
|
Assert.Null(canonical.Snapshot.ParentGuid);
|
|
Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Round 5 R5-2: cancellation-aware detach/restore window, both
|
|
// deferred buckets.
|
|
// ---------------------------------------------------------------
|
|
|
|
// Reviewer scenario (a) for the CREATES bucket: two children queued
|
|
// behind one missing parent; C1's own registration callback delivers
|
|
// DeleteObject(C2) then DeleteObject(parent) - C2 must NOT be restored,
|
|
// the recreated parent drains nothing, and every ledger converges.
|
|
[Fact]
|
|
public void DeferredChildReplayWindowFiltersASiblingDeletedMidReplayFromTheRestoredRemainder()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 109UL);
|
|
const uint parentGuid = 0x70045000u;
|
|
const uint firstChildGuid = 0x70045001u;
|
|
const uint secondChildGuid = 0x70045002u;
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(firstChildGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(secondChildGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
|
|
RuntimeEntityRecord? firstChild = null;
|
|
WrapDeferredChildRegistration(lifetime, (original, spawn, isLocalPlayer) =>
|
|
{
|
|
RuntimeEntityRegistrationResult result = original(spawn, isLocalPlayer);
|
|
if (spawn.Guid == firstChildGuid)
|
|
{
|
|
firstChild = result.Canonical;
|
|
// C2 was never individually registered (still a raw
|
|
// deferred blob) - its own delete legitimately reports
|
|
// false (TryAcceptDelete's known-object gate never
|
|
// accepted it), but CancelDeferredChildGeneration still
|
|
// runs unconditionally before that gate and records the
|
|
// window filter.
|
|
_ = lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(secondChildGuid, 1), isLocalPlayer: false,
|
|
removeRetainedObject: true, out _);
|
|
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(parentGuid, 1), isLocalPlayer: false,
|
|
removeRetainedObject: true, out RuntimeEntityDeleteAcceptance parentAcceptance));
|
|
lifetime.CompleteAcceptedDelete(parentAcceptance);
|
|
}
|
|
return result;
|
|
});
|
|
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedAuthority,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
|
|
Assert.Equal(default, receipt);
|
|
Assert.True(lifetime.Entities.TryGetActive(firstChildGuid, out _));
|
|
Assert.False(lifetime.Entities.ParentAttachments.ContainsDeferredCreate(secondChildGuid, 1));
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredCreateCount);
|
|
|
|
// C1 was itself admitted as its own ("Parented") initial-residence
|
|
// registration - a REAL entity, not a raw blob - and that lease is
|
|
// independent of the parentGuid-keyed deferred-create window this
|
|
// test targets. Drain it too so every ledger genuinely converges
|
|
// (round5-fixes.md's own "ledgers converge" wording), rather than
|
|
// leaving an unrelated open lease that has nothing to do with the
|
|
// window/filter mechanism under test.
|
|
Assert.NotNull(firstChild);
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
firstChild!, out RuntimeInitialCreateResidenceLease firstChildLease));
|
|
_ = RunToCompletion(lifetime, firstChild!, firstChildLease.Token, NoContact);
|
|
|
|
RuntimeEntityRecord recreatedParent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 2, includePosition: false), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
recreatedParent, out RuntimeInitialCreateResidenceLease recreatedLease));
|
|
RuntimeInitialCreateExecutionReceipt recreatedReceipt = RunToCompletion(
|
|
lifetime, recreatedParent, recreatedLease.Token, NoContact);
|
|
Assert.Equal(0, recreatedReceipt.ReplayedDeferredChildCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
}
|
|
|
|
// Reviewer scenario (b) for the CREATES bucket: C1's callback drives a
|
|
// full session clear - restore no-ops, DeferredParentCreateCount stays
|
|
// 0, IsConverged holds after teardown.
|
|
[Fact]
|
|
public void DeferredChildReplayWindowReleasesSilentlyWhenSessionClearFiresMidReplay()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 110UL);
|
|
const uint parentGuid = 0x70046000u;
|
|
const uint firstChildGuid = 0x70046001u;
|
|
const uint secondChildGuid = 0x70046002u;
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(firstChildGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(secondChildGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
|
|
WrapDeferredChildRegistration(lifetime, (original, spawn, isLocalPlayer) =>
|
|
{
|
|
RuntimeEntityRegistrationResult result = original(spawn, isLocalPlayer);
|
|
if (spawn.Guid == firstChildGuid)
|
|
{
|
|
IReadOnlyList<RuntimeEntityRecord> retirements = lifetime.BeginSessionClear();
|
|
foreach (RuntimeEntityRecord record in retirements)
|
|
lifetime.CompleteSessionEntityRetirement(record);
|
|
}
|
|
return result;
|
|
});
|
|
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedAuthority,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
|
|
Assert.Equal(default, receipt);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
Assert.True(lifetime.CompleteSessionClearIfConverged());
|
|
}
|
|
|
|
// Reviewer scenario (a), relation-queue flavor: two queued relations
|
|
// for the same parent; C1's own attach-commit publish delivers
|
|
// DeleteObject(C2) then DeleteObject(parent) - C2's relation must NOT
|
|
// be restored.
|
|
[Fact]
|
|
public void DeferredAcceptedRelationReplayWindowFiltersASiblingRelationDeletedMidReplay()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 111UL);
|
|
const uint parentGuid = 0x70047000u;
|
|
const uint firstChildGuid = 0x70047001u;
|
|
const uint secondChildGuid = 0x70047002u;
|
|
|
|
RuntimeEntityRecord firstChild = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(firstChildGuid, 1, includePosition: false), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(firstChild, out RuntimeInitialCreateResidenceLease firstLease));
|
|
RuntimeEntityRecord secondChild = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(secondChildGuid, 1, includePosition: false), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(secondChild, out RuntimeInitialCreateResidenceLease secondLease));
|
|
|
|
// Both continuations admit while the FIRST parent incarnation is
|
|
// addressable, then the parent is orphaned once for both.
|
|
// includePosition:false + RemoveActive (not a full DeleteObject-
|
|
// acceptance) for the same reason AdmitThenOrphanParentRelation uses
|
|
// them: a real delete retains a permanent per-(guid,generation)
|
|
// teardown tombstone and a position-bearing retained snapshot would
|
|
// bleed into the SAME-incarnation "recovered CreateObject" merge
|
|
// when parentGuid is re-registered at incarnation 1 again below.
|
|
RuntimeEntityRecord orphanedParent = lifetime.RegisterEntity(
|
|
Spawn(parentGuid, 1, includePosition: false)).Canonical!;
|
|
var firstParentUpdate = new ParentEvent.Parsed(
|
|
parentGuid, firstChildGuid, ParentLocation: 1u, PlacementId: 0u,
|
|
ParentInstanceSequence: 1, ChildPositionSequence: 2);
|
|
Assert.True(lifetime.TryApplyParent(firstParentUpdate, null, out _));
|
|
var secondParentUpdate = new ParentEvent.Parsed(
|
|
parentGuid, secondChildGuid, ParentLocation: 1u, PlacementId: 0u,
|
|
ParentInstanceSequence: 1, ChildPositionSequence: 2);
|
|
Assert.True(lifetime.TryApplyParent(secondParentUpdate, null, out _));
|
|
Assert.True(lifetime.Entities.RemoveActive(orphanedParent));
|
|
|
|
_ = RunToCompletion(lifetime, firstChild, firstLease.Token, NoContact);
|
|
_ = RunToCompletion(lifetime, secondChild, secondLease.Token, NoContact);
|
|
|
|
Assert.Equal(2, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
|
|
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
if (delta.Change is not RuntimeEntityChange.Updated
|
|
|| delta.Entity.Identity.ServerGuid != firstChildGuid)
|
|
{
|
|
return;
|
|
}
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(secondChildGuid, 1), isLocalPlayer: false,
|
|
removeRetainedObject: true, out RuntimeEntityDeleteAcceptance secondAcceptance));
|
|
lifetime.CompleteAcceptedDelete(secondAcceptance);
|
|
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(parentGuid, 1), isLocalPlayer: false,
|
|
removeRetainedObject: true, out RuntimeEntityDeleteAcceptance parentAcceptance));
|
|
lifetime.CompleteAcceptedDelete(parentAcceptance);
|
|
}));
|
|
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedAuthority,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
|
|
Assert.Equal(default, receipt);
|
|
Assert.False(lifetime.Entities.ParentAttachments
|
|
.ContainsDeferredAcceptedRelation(secondChildGuid, secondChild.Key!.Value));
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// Reviewer scenario (b), relation-queue flavor: C1's own attach-commit
|
|
// publish drives a full session clear - restore no-ops for C2's
|
|
// relation, converges.
|
|
[Fact]
|
|
public void DeferredAcceptedRelationReplayWindowReleasesSilentlyWhenSessionClearFiresMidReplay()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 112UL);
|
|
const uint parentGuid = 0x70048000u;
|
|
const uint firstChildGuid = 0x70048001u;
|
|
const uint secondChildGuid = 0x70048002u;
|
|
|
|
RuntimeEntityRecord firstChild = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(firstChildGuid, 1, includePosition: false), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(firstChild, out RuntimeInitialCreateResidenceLease firstLease));
|
|
RuntimeEntityRecord secondChild = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(secondChildGuid, 1, includePosition: false), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(secondChild, out RuntimeInitialCreateResidenceLease secondLease));
|
|
|
|
// includePosition:false + RemoveActive (not a full DeleteObject-
|
|
// acceptance) for the same reason AdmitThenOrphanParentRelation uses
|
|
// them: a real delete retains a permanent per-(guid,generation)
|
|
// teardown tombstone and a position-bearing retained snapshot would
|
|
// bleed into the SAME-incarnation "recovered CreateObject" merge
|
|
// when parentGuid is re-registered at incarnation 1 again below -
|
|
// and the reentrant BeginSessionClear below retires every currently
|
|
// active record (including the recreated parent), which would
|
|
// collide with a stale tombstone left by a full delete here.
|
|
RuntimeEntityRecord orphanedParent = lifetime.RegisterEntity(
|
|
Spawn(parentGuid, 1, includePosition: false)).Canonical!;
|
|
var firstParentUpdate = new ParentEvent.Parsed(
|
|
parentGuid, firstChildGuid, ParentLocation: 1u, PlacementId: 0u,
|
|
ParentInstanceSequence: 1, ChildPositionSequence: 2);
|
|
Assert.True(lifetime.TryApplyParent(firstParentUpdate, null, out _));
|
|
var secondParentUpdate = new ParentEvent.Parsed(
|
|
parentGuid, secondChildGuid, ParentLocation: 1u, PlacementId: 0u,
|
|
ParentInstanceSequence: 1, ChildPositionSequence: 2);
|
|
Assert.True(lifetime.TryApplyParent(secondParentUpdate, null, out _));
|
|
Assert.True(lifetime.Entities.RemoveActive(orphanedParent));
|
|
|
|
_ = RunToCompletion(lifetime, firstChild, firstLease.Token, NoContact);
|
|
_ = RunToCompletion(lifetime, secondChild, secondLease.Token, NoContact);
|
|
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
if (delta.Change is not RuntimeEntityChange.Updated
|
|
|| delta.Entity.Identity.ServerGuid != firstChildGuid)
|
|
{
|
|
return;
|
|
}
|
|
IReadOnlyList<RuntimeEntityRecord> retirements = lifetime.BeginSessionClear();
|
|
foreach (RuntimeEntityRecord record in retirements)
|
|
lifetime.CompleteSessionEntityRetirement(record);
|
|
}));
|
|
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedAuthority,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
|
|
Assert.Equal(default, receipt);
|
|
Assert.Equal(0, lifetime.Entities.ParentAttachments.DeferredAcceptedRelationCount);
|
|
Assert.True(lifetime.CompleteSessionClearIfConverged());
|
|
}
|
|
|
|
// Round 3 E-matrix (a)+(d): TRUE ParentAttachmentState semantics, verified
|
|
// by reading the source before writing this test. ParentAttachmentState.
|
|
// DeleteGeneration(parentGuid, oldIncarnation) - called from
|
|
// TryAcceptDelete when the PARENT is what's being deleted - only ever
|
|
// touches the _unresolvedByChild ParentEvent-relation queue (via
|
|
// FilterParentCandidates, keeping relations addressed to a STRICTLY
|
|
// newer parent incarnation). It never touches _deferredCreatesByParent,
|
|
// the raw-netblob bucket ReplayDeferredChildren/DetachDeferredCreates
|
|
// actually replays - that bucket is keyed ONLY by parent GUID, with no
|
|
// per-incarnation filtering at all (PhysicsAttachment carries no parent
|
|
// instance sequence to filter by). So a child queued while parent
|
|
// incarnation 1 was missing survives the parent's own delete untouched,
|
|
// and DOES replay against a same-GUID recreated incarnation 2 - this
|
|
// mirrors retail's own GUID-keyed netblob dispatch (see
|
|
// ReplayDeferredChildren's remarks: "a parent's own successful Create
|
|
// replays every blob queued waiting on ITS guid"), not a defect.
|
|
[Fact]
|
|
public void DeferredChildQueuedForDeletedParentIncarnationStillReplaysAgainstTheRecreatedParentGuid()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 50UL);
|
|
const uint parentGuid = 0x70030100u;
|
|
const uint childGuid = 0x70030000u;
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
|
|
// Parent incarnation 1 arrives, then is deleted, WITHOUT ever
|
|
// completing a residence (simulating "gone before the child's
|
|
// replay").
|
|
RuntimeEntityRecord parentGen1 = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(parentGuid, 1),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: true,
|
|
out RuntimeEntityDeleteAcceptance acceptance));
|
|
lifetime.CompleteAcceptedDelete(acceptance);
|
|
Assert.False(lifetime.Entities.IsCurrent(parentGen1));
|
|
// The deferred child create survives the parent's delete untouched -
|
|
// the TRUE, verified behavior.
|
|
Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
|
|
// Parent GUID reused at a NEWER incarnation.
|
|
RuntimeEntityRecord parentGen2 = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 2, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parentGen2, out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, parentGen2, parentLease.Token, NoContact);
|
|
|
|
Assert.Equal(1, receipt.ReplayedDeferredChildCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord child));
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(child, out _));
|
|
}
|
|
|
|
// Round 3 E-matrix (b): a child's own exact Delete before the parent's
|
|
// replay cancels only that child (CancelDeferredChildGeneration's side
|
|
// effect runs even though TryAcceptDelete's overall return is false for
|
|
// a guid with no seeded gate - a purely-deferred child was never
|
|
// registered, so it never got one).
|
|
[Fact]
|
|
public void ChildExactDeleteBeforeParentReplayCancelsOnlyThatChild()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 51UL);
|
|
const uint parentGuid = 0x70030200u;
|
|
const uint firstChildGuid = 0x70030300u;
|
|
const uint secondChildGuid = 0x70030400u;
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(firstChildGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(secondChildGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.Equal(2, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
|
|
// The purely-deferred child has no gate; TryAcceptDelete's overall
|
|
// gate-consuming path fails, but the deferred-create cancellation
|
|
// side effect at the top of the method already ran.
|
|
Assert.False(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(firstChildGuid, 1),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: true,
|
|
out _));
|
|
Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, parent, parentLease.Token, NoContact);
|
|
|
|
Assert.Equal(1, receipt.ReplayedDeferredChildCount);
|
|
Assert.False(lifetime.Entities.TryGetActive(firstChildGuid, out _));
|
|
Assert.True(lifetime.Entities.TryGetActive(secondChildGuid, out _));
|
|
Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
}
|
|
|
|
// Round 3 E-matrix (c): two raw Creates queued for the SAME child guid
|
|
// (instance 1 and instance 2, both waiting on the same missing parent).
|
|
// Deleting the child at instance 1 cancels only the older, same-or-older
|
|
// queued generation; the strictly-newer instance 2 survives and replays.
|
|
[Fact]
|
|
public void NewerDeferredChildGenerationSurvivesOlderGenerationCleanupAndReplays()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 52UL);
|
|
const uint parentGuid = 0x70030500u;
|
|
const uint childGuid = 0x70030600u;
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 2, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.Equal(2, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
|
|
Assert.False(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(childGuid, 1),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: true,
|
|
out _));
|
|
Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, parent, parentLease.Token, NoContact);
|
|
|
|
Assert.Equal(1, receipt.ReplayedDeferredChildCount);
|
|
Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord child));
|
|
Assert.Equal(2, child.Incarnation);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
}
|
|
|
|
// Round 3 E-matrix (e): child GUID reuse across a COMPLETE lifecycle -
|
|
// register+replay+run-to-completion the first incarnation, delete it
|
|
// fully (gate removed), then reuse the SAME guid for a fresh incarnation
|
|
// that is AGAIN deferred (different missing parent) and replays cleanly
|
|
// with no leakage from the torn-down first generation.
|
|
[Fact]
|
|
public void ChildGuidReuseAfterFullLifecycleReplaysCleanlyWithoutStaleState()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 53UL);
|
|
const uint firstParentGuid = 0x70030700u;
|
|
const uint secondParentGuid = 0x70030800u;
|
|
const uint childGuid = 0x70030900u;
|
|
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false, parentGuid: firstParentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
RuntimeEntityRecord firstParent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(firstParentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
firstParent, out RuntimeInitialCreateResidenceLease firstParentLease));
|
|
_ = RunToCompletion(lifetime, firstParent, firstParentLease.Token, NoContact);
|
|
Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord firstChild));
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(firstChild, out RuntimeInitialCreateResidenceLease childLease));
|
|
_ = RunToCompletion(lifetime, firstChild, childLease.Token, NoContact);
|
|
|
|
// Fully delete the first incarnation (its gate is removed too).
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(childGuid, 1),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: true,
|
|
out RuntimeEntityDeleteAcceptance deleteAcceptance));
|
|
lifetime.CompleteAcceptedDelete(deleteAcceptance);
|
|
Assert.False(lifetime.Entities.TryGetActive(childGuid, out _));
|
|
|
|
// The SAME guid reused for a fresh incarnation, deferred behind a
|
|
// DIFFERENT missing parent.
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 2, includePosition: false, parentGuid: secondParentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
|
|
RuntimeEntityRecord secondParent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(secondParentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
secondParent, out RuntimeInitialCreateResidenceLease secondParentLease));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, secondParent, secondParentLease.Token, NoContact);
|
|
|
|
Assert.Equal(1, receipt.ReplayedDeferredChildCount);
|
|
Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord reusedChild));
|
|
Assert.Equal(2, reusedChild.Incarnation);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
}
|
|
|
|
// Round 3 E-matrix (f): a full session reset before the missing parent
|
|
// ever arrives clears the deferred-create bucket to zero.
|
|
[Fact]
|
|
public void SessionResetBeforeParentArrivalClearsDeferredBucket()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 54UL);
|
|
const uint parentGuid = 0x70030A00u;
|
|
const uint childGuid = 0x70030B00u;
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
|
|
IReadOnlyList<RuntimeEntityRecord> retirements = lifetime.BeginSessionClear();
|
|
Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
foreach (RuntimeEntityRecord record in retirements)
|
|
lifetime.CompleteSessionEntityRetirement(record);
|
|
Assert.True(lifetime.CompleteSessionClearIfConverged());
|
|
}
|
|
|
|
// Round 3 E-matrix (g): a full session reset triggered REENTRANTLY from
|
|
// inside the deferred-child replay's own Registered callback. The
|
|
// parent's own in-flight Execute() must abandon cleanly rather than
|
|
// resurrect anything, and the session-clear transaction itself must
|
|
// still converge once its own retirements are drained.
|
|
[Fact]
|
|
public void SessionResetFromWithinReplayDrivenCallbackConverges()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 55UL);
|
|
const uint parentGuid = 0x70030C00u;
|
|
const uint childGuid = 0x70030D00u;
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
IReadOnlyList<RuntimeEntityRecord>? retirements = null;
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
if (delta.Change is not RuntimeEntityChange.Registered
|
|
|| delta.Entity.Identity.ServerGuid != childGuid
|
|
|| retirements is not null)
|
|
{
|
|
return;
|
|
}
|
|
retirements = lifetime.BeginSessionClear();
|
|
}));
|
|
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt);
|
|
|
|
// The parent's own in-flight execution is no longer current once the
|
|
// reentrant reset retires everything - a typed abandonment, not a
|
|
// resurrection or an escaping exception.
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.RejectedAuthority, status);
|
|
Assert.Equal(default, receipt);
|
|
Assert.NotNull(retirements);
|
|
foreach (RuntimeEntityRecord record in retirements!)
|
|
lifetime.CompleteSessionEntityRetirement(record);
|
|
Assert.True(lifetime.CompleteSessionClearIfConverged());
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
}
|
|
|
|
// Round 3 E-matrix (h): instance sequence zero is a normal retail
|
|
// timestamp, not an empty sentinel (see ParentAttachmentState's own
|
|
// remarks on CancelDeferredChildGeneration) - a deferred child at
|
|
// incarnation 0 must replay exactly like any other incarnation.
|
|
[Fact]
|
|
public void InstanceSequenceZeroChildReplaysCorrectly()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 56UL);
|
|
const uint parentGuid = 0x70030E00u;
|
|
const uint childGuid = 0x70030F00u;
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 0, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
Assert.Equal(1, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent, out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, parent, parentLease.Token, NoContact);
|
|
|
|
Assert.Equal(1, receipt.ReplayedDeferredChildCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().DeferredParentCreateCount);
|
|
Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord child));
|
|
Assert.Equal(0, child.Incarnation);
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(child, out _));
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// F. Failure boundaries.
|
|
//
|
|
// (b) delete from the hook-emission window (after InitialAdoption,
|
|
// before the FIFO drain) has NO reachable callback of its own:
|
|
// RunInitialTail's Adopted phase (InitialAdoption) and HookRecorded
|
|
// phase (the AfterEnterWorld teleport-hook trace entry) are both pure
|
|
// trace-only mutations with no event dispatch - there is no observer
|
|
// boundary between them. The ONLY publish inside RunInitialTail is
|
|
// ReplayDeferredChildren's per-child Registered event (the
|
|
// DeferredReplayed phase), which already has dedicated coverage:
|
|
// DeleteDuringDeferredChildReplayAbandonsExecutionWithoutResurrection
|
|
// (above, in section G) deletes reentrantly from exactly that callback
|
|
// and proves the abandonment converges without resurrecting anything.
|
|
// Adding a second, structurally-identical test here would only
|
|
// duplicate that coverage; the closest reachable boundary already has a
|
|
// test.
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void DeleteFromObserverBeforeInitialAdoptionAbandonsFirstExecuteCleanly()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 60UL);
|
|
const uint guid = 0x70031000u;
|
|
|
|
// RegisterEntityCore's InitializeAcceptedCreateResidence (Begin())
|
|
// runs BEFORE PublishEntity(Registered, ...) - a real lease/token
|
|
// already exists by the time this observer fires, so it can be
|
|
// captured and later handed to Execute() to observe that FIRST
|
|
// call's own outcome directly, rather than merely proving no lease
|
|
// survives.
|
|
RuntimeEntityRecord? capturedCanonical = null;
|
|
RuntimeInitialCreateResidenceLease capturedLease = default;
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
if (delta.Change is not RuntimeEntityChange.Registered
|
|
|| delta.Entity.Identity.ServerGuid != guid)
|
|
{
|
|
return;
|
|
}
|
|
Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord active));
|
|
capturedCanonical = active;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(active, out capturedLease));
|
|
// Reentrant delete BEFORE Execute() is ever called for this
|
|
// entity at all - before RunInitialTail's InitialAdoption phase,
|
|
// the very first thing an Execute() call would otherwise do.
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(guid, active.Incarnation),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: true,
|
|
out RuntimeEntityDeleteAcceptance acceptance));
|
|
lifetime.CompleteAcceptedDelete(acceptance);
|
|
}));
|
|
|
|
// The reentrant delete bumps the lifetime-mutation counter for this
|
|
// guid DURING the Registered publish, so RegisterEntityCore's own
|
|
// post-publish currency check reports this registration as
|
|
// superseded (Canonical: null in the returned result) - the record
|
|
// captured from inside the observer, above, is the one to assert
|
|
// against.
|
|
_ = lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false);
|
|
Assert.NotNull(capturedCanonical);
|
|
RuntimeEntityRecord canonical = capturedCanonical!;
|
|
Assert.False(lifetime.Entities.IsCurrent(canonical));
|
|
// TryAcceptDelete's own ForgetInitialCreateResidence already retired
|
|
// the residence reentrantly - nothing was ever adopted.
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
using IDisposable secondSubscription = lifetime.Events.Subscribe(
|
|
new EntityObserver(delta => observed.Add(delta.Change)));
|
|
|
|
// The FIRST (and only) Execute() call for this entity, using the
|
|
// token captured before the reentrant delete: the residence is
|
|
// already gone (RejectedToken, not RejectedAuthority - there was
|
|
// never a Progress to Abandon), publishes nothing new, and
|
|
// converges.
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedToken,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
canonical, capturedLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
|
|
Assert.Equal(default, receipt);
|
|
Assert.Empty(observed);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void DeleteFromObserverBetweenFifoEntriesAppliesFirstOnlyAndConverges()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 61UL);
|
|
const uint guid = 0x70031100u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
var appearance = new ObjDescEvent.Parsed(
|
|
guid,
|
|
new CreateObject.ModelData(
|
|
0x09000002u,
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.AnimPartChange>()),
|
|
InstanceSequence: 1,
|
|
ObjDescSequence: 2);
|
|
Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
|
|
var vector = new VectorUpdate.Parsed(
|
|
guid, new Vector3(11f, 12f, 13f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
|
|
Assert.True(lifetime.TryApplyVector(vector, null, out _));
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
bool deletedFromObserver = false;
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
observed.Add(delta.Change);
|
|
if (deletedFromObserver || delta.Change is not RuntimeEntityChange.Updated)
|
|
return;
|
|
deletedFromObserver = true;
|
|
// Between FIFO entry 1 (ObjDesc, already applied+published) and
|
|
// entry 2 (Vector, not yet reached) - delete reentrantly.
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(guid, canonical.Incarnation),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: true,
|
|
out RuntimeEntityDeleteAcceptance acceptance));
|
|
lifetime.CompleteAcceptedDelete(acceptance);
|
|
}));
|
|
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt);
|
|
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.RejectedAuthority, status);
|
|
Assert.Equal(default, receipt);
|
|
// ObjDesc applied+published exactly once; Vector never applied
|
|
// (canonical's velocity stays at its pre-drain value); the delete's
|
|
// own Deleted is the only other observed event.
|
|
Assert.Equal(
|
|
[RuntimeEntityChange.Updated, RuntimeEntityChange.Deleted],
|
|
observed);
|
|
Assert.Equal(0x09000002u, canonical.Snapshot.BasePaletteId);
|
|
Assert.Null(canonical.Snapshot.Physics!.Value.Velocity);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
|
|
// A stale retry never resurrects anything or reapplies Vector.
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedToken,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt retryReceipt));
|
|
Assert.Equal(default, retryReceipt);
|
|
Assert.Null(canonical.Snapshot.Physics!.Value.Velocity);
|
|
}
|
|
|
|
// Round 3 F(d): an observer that throws during a publish must not
|
|
// corrupt the drain or escape Execute() - RuntimeEntityObjectEventStream.
|
|
// Dispatch contains each observer's exception per-call (RecordDispatchFailure)
|
|
// and continues to the next observer/pending item. Pin that actual,
|
|
// verified behavior here rather than assuming propagation.
|
|
[Fact]
|
|
public void ObserverThrowDuringPublishIsContainedAndDrainConvergesWithNoDuplicateRetry()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 62UL);
|
|
const uint guid = 0x70031200u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
var vector = new VectorUpdate.Parsed(
|
|
guid, Vector3.One, Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
|
|
Assert.True(lifetime.TryApplyVector(vector, null, out _));
|
|
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
throw new InvalidOperationException("Deliberate observer failure for F(d).")));
|
|
long failuresBefore = lifetime.Events.DispatchFailureCount;
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Contains(
|
|
RuntimeInitialCreateExecutedActionKind.Vector,
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
Assert.Equal(Vector3.One, canonical.Snapshot.Physics!.Value.Velocity);
|
|
Assert.True(lifetime.Events.DispatchFailureCount > failuresBefore);
|
|
Assert.NotNull(lifetime.Events.LastDispatchFailure);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
|
|
// A stale retry after the (contained) throw does not duplicate any
|
|
// side effect.
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedToken,
|
|
lifetime.InitialCreateExecution.Execute(canonical, lease.Token, NoContact, out _));
|
|
Assert.Equal(Vector3.One, canonical.Snapshot.Physics!.Value.Velocity);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// G. Reentrancy
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void ReentrantExecuteForTheSameEntityFailsClosedRatherThanInterleaving()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 11UL);
|
|
const uint guid = 0x70026000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
var reentrantStatuses = new List<RuntimeInitialCreateExecutionStatus>();
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
if (delta.Change is not RuntimeEntityChange.Updated || reentrantStatuses.Count != 0)
|
|
return;
|
|
// Firing a nested Execute from inside an event observer while the
|
|
// outer Execute is still on the stack must fail closed.
|
|
reentrantStatuses.Add(lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out _));
|
|
}));
|
|
var vector = new VectorUpdate.Parsed(
|
|
guid, Vector3.One, Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
|
|
Assert.True(lifetime.TryApplyVector(vector, null, out _));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Equal(
|
|
[RuntimeInitialCreateExecutionStatus.RejectedAuthority],
|
|
reentrantStatuses);
|
|
Assert.NotEmpty(receipt.Trace);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void DeleteDuringDeferredChildReplayAbandonsExecutionWithoutResurrection()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 12UL);
|
|
const uint parentGuid = 0x70026100u;
|
|
const uint childGuid = 0x70026200u;
|
|
Assert.True(lifetime.RegisterEntityWithInitialResidence(
|
|
Spawn(childGuid, 1, includePosition: false, parentGuid: parentGuid),
|
|
isLocalPlayer: false)
|
|
.DeferredForParent);
|
|
RuntimeEntityRecord parent = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(parentGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
parent,
|
|
out RuntimeInitialCreateResidenceLease parentLease));
|
|
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
if (delta.Change is not RuntimeEntityChange.Registered
|
|
|| delta.Entity.Identity.ServerGuid != childGuid)
|
|
{
|
|
return;
|
|
}
|
|
// A reentrant delete of the PARENT itself, triggered from inside
|
|
// the child-registration callback the deferred replay drives.
|
|
Assert.True(lifetime.TryAcceptDelete(
|
|
new DeleteObject.Parsed(parentGuid, parent.Incarnation),
|
|
isLocalPlayer: false,
|
|
removeRetainedObject: true,
|
|
out RuntimeEntityDeleteAcceptance acceptance));
|
|
lifetime.CompleteAcceptedDelete(acceptance);
|
|
}));
|
|
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
parent, parentLease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt);
|
|
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.RejectedAuthority, status);
|
|
Assert.Equal(default, receipt);
|
|
Assert.False(lifetime.Entities.IsCurrent(parent));
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
// Only the PARENT's own residence/progress was ever touched by this
|
|
// abandoned Execute call. The CHILD is a distinct entity the
|
|
// deferred replay legitimately registered through the canonical
|
|
// route before the reentrant delete happened - its own (never
|
|
// executed) residence lease correctly remains open; deleting the
|
|
// parent does not cascade-tear-down an unrelated child.
|
|
Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.True(lifetime.Entities.TryGetActive(childGuid, out RuntimeEntityRecord child));
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(child, out _));
|
|
}
|
|
|
|
// Round 3 B2 regression (reentrancy from a publish callback): a wire
|
|
// apply (TryApplyMotion) reentrantly enqueued from inside an EARLIER
|
|
// continuation's own publish must NOT retire the residence, and the
|
|
// continuation that was ALREADY queued before the drain even started
|
|
// (Vector) must still drain - proving the reentrant enqueue never
|
|
// displaces or skips the pre-existing FIFO tail.
|
|
[Fact]
|
|
public void WireApplyDuringDrainPublishDoesNotRetireResidenceAndPreExistingFifoStillDrains()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 63UL);
|
|
const uint guid = 0x70031300u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
var appearance = new ObjDescEvent.Parsed(
|
|
guid,
|
|
new CreateObject.ModelData(
|
|
0x0A000002u,
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.AnimPartChange>()),
|
|
InstanceSequence: 1,
|
|
ObjDescSequence: 2);
|
|
Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
|
|
var vector = new VectorUpdate.Parsed(
|
|
guid, new Vector3(21f, 22f, 23f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
|
|
Assert.True(lifetime.TryApplyVector(vector, null, out _));
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
bool reentered = false;
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
observed.Add(delta.Change);
|
|
if (reentered || delta.Change is not RuntimeEntityChange.Updated)
|
|
return;
|
|
reentered = true;
|
|
var motion = new WorldSession.EntityMotionUpdate(
|
|
guid,
|
|
new CreateObject.ServerMotionState(0x3d, 0x11),
|
|
InstanceSequence: 1,
|
|
MovementSequence: 2,
|
|
ServerControlSequence: 1,
|
|
IsAutonomous: false);
|
|
Assert.True(lifetime.TryApplyMotion(motion, retainPayload: true, null, out _, out _));
|
|
}));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
// Vector (already queued before the drain started) drains BEFORE
|
|
// the reentrantly-enqueued Motion, and both drain exactly once.
|
|
Assert.Equal(
|
|
[
|
|
RuntimeInitialCreateExecutedActionKind.InitialAdoption,
|
|
RuntimeInitialCreateExecutedActionKind.ObjDesc,
|
|
RuntimeInitialCreateExecutedActionKind.Vector,
|
|
RuntimeInitialCreateExecutedActionKind.Movement,
|
|
],
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
Assert.Equal(
|
|
[RuntimeEntityChange.Updated, RuntimeEntityChange.Updated, RuntimeEntityChange.Updated],
|
|
observed);
|
|
Assert.Equal(new Vector3(21f, 22f, 23f), canonical.Snapshot.Physics!.Value.Velocity);
|
|
Assert.Equal(new CreateObject.ServerMotionState(0x3d, 0x11), canonical.Snapshot.MotionState);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void RegisterDifferentEntityFromCallbackDuringDrainConvergesBothIndependently()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 64UL);
|
|
const uint firstGuid = 0x70031400u;
|
|
const uint secondGuid = 0x70031500u;
|
|
RuntimeEntityRecord first = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(firstGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
first, out RuntimeInitialCreateResidenceLease firstLease));
|
|
var vector = new VectorUpdate.Parsed(
|
|
firstGuid, Vector3.One, Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
|
|
Assert.True(lifetime.TryApplyVector(vector, null, out _));
|
|
|
|
RuntimeEntityRecord? secondCanonical = null;
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
if (delta.Change is not RuntimeEntityChange.Updated
|
|
|| delta.Entity.Identity.ServerGuid != firstGuid
|
|
|| secondCanonical is not null)
|
|
{
|
|
return;
|
|
}
|
|
// A completely unrelated entity registered reentrantly, from
|
|
// inside the FIRST entity's own drain publish.
|
|
secondCanonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(secondGuid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
}));
|
|
|
|
RuntimeInitialCreateExecutionReceipt firstReceipt = RunToCompletion(
|
|
lifetime, first, firstLease.Token, NoContact);
|
|
|
|
Assert.NotEmpty(firstReceipt.Trace);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.NotNull(secondCanonical);
|
|
Assert.True(lifetime.Entities.IsCurrent(secondCanonical!));
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(secondCanonical!, out RuntimeInitialCreateResidenceLease secondLease));
|
|
|
|
// The second entity's own independent residence still drains
|
|
// normally afterward.
|
|
RuntimeInitialCreateExecutionReceipt secondReceipt = RunToCompletion(
|
|
lifetime, secondCanonical!, secondLease.Token, NoContact);
|
|
Assert.Contains(
|
|
RuntimeInitialCreateExecutedActionKind.InitialAdoption,
|
|
secondReceipt.Trace.Select(static a => a.Kind));
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// Round 3 G(c)/I2: recreating the SAME guid at a NEWER incarnation from
|
|
// a reentrant callback mid-drain. The old execution must abandon
|
|
// cleanly (its own progress+residence converge) with NO FIFO transfer
|
|
// to the new incarnation; the new incarnation is a completely
|
|
// independent, unaffected registration. This is the same scenario the
|
|
// task's "replacement convergence" item names - one test covers both.
|
|
[Fact]
|
|
public void RecreateSameGuidNewerIncarnationFromCallbackAbandonsOldExecutionWithoutFifoTransfer()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 65UL);
|
|
const uint guid = 0x70031600u;
|
|
RuntimeEntityRecord canonicalGen1 = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonicalGen1, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
var appearance = new ObjDescEvent.Parsed(
|
|
guid,
|
|
new CreateObject.ModelData(
|
|
0x0B000002u,
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.AnimPartChange>()),
|
|
InstanceSequence: 1,
|
|
ObjDescSequence: 2);
|
|
Assert.True(lifetime.TryApplyObjDesc(appearance, null, out _));
|
|
var vector = new VectorUpdate.Parsed(
|
|
guid, new Vector3(31f, 32f, 33f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
|
|
Assert.True(lifetime.TryApplyVector(vector, null, out _));
|
|
|
|
bool recreated = false;
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
if (recreated || delta.Change is not RuntimeEntityChange.Updated)
|
|
return;
|
|
recreated = true;
|
|
// Same guid, NEWER incarnation, from inside the FIRST
|
|
// continuation's (ObjDesc's) own publish - a bare Create with no
|
|
// residence, to isolate the replacement mechanics from any
|
|
// second drain.
|
|
_ = lifetime.RegisterEntity(Spawn(guid, 2, includePosition: false));
|
|
}));
|
|
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonicalGen1, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt);
|
|
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.RejectedAuthority, status);
|
|
Assert.Equal(default, receipt);
|
|
Assert.False(lifetime.Entities.IsCurrent(canonicalGen1));
|
|
// Old progress+residence fully converge.
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
|
|
// The new incarnation is unaffected: no residence pending (a bare
|
|
// RegisterEntity never begins one), and none of generation 1's
|
|
// FIFO (Vector, still undrained) ever transferred to it.
|
|
Assert.True(lifetime.Entities.TryGetActive(guid, out RuntimeEntityRecord canonicalGen2));
|
|
Assert.Equal(2, canonicalGen2.Incarnation);
|
|
Assert.False(lifetime.TryGetInitialCreateResidence(canonicalGen2, out _));
|
|
Assert.Null(canonicalGen2.Snapshot.Physics!.Value.Velocity);
|
|
}
|
|
|
|
// Round 3 G(d): Dispose() called reentrantly from a publish callback.
|
|
// Pin the ACTUAL observed behavior (verified by running this test)
|
|
// rather than assuming either containment or propagation.
|
|
[Fact]
|
|
public void DisposeFromCallbackDuringDrainConvergesTheCompleteLedger()
|
|
{
|
|
var lifetime = new RuntimeEntityObjectLifetime();
|
|
Bind(lifetime, 66UL);
|
|
const uint guid = 0x70031700u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
var vector = new VectorUpdate.Parsed(
|
|
guid, Vector3.One, Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
|
|
Assert.True(lifetime.TryApplyVector(vector, null, out _));
|
|
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
if (delta.Change is RuntimeEntityChange.Updated)
|
|
lifetime.Dispose();
|
|
}));
|
|
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt);
|
|
|
|
// Dispose() reentrantly retires everything (BeginSessionClear ->
|
|
// teardown -> converge) before the outer Execute() call's own
|
|
// currency check runs again; that check sees a no-longer-current
|
|
// canonical and abandons in the SAME typed way any other reentrant
|
|
// teardown does. No invalid enumeration/exception escapes.
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.RejectedAuthority, status);
|
|
Assert.Equal(default, receipt);
|
|
Assert.True(lifetime.CaptureOwnership().IsConverged);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// H. Saturation / stale-progress
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void StaleProgressLeaseIdIsDiscardedAndFailsClosedThenRetrySucceedsCleanly()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 13UL);
|
|
const uint guid = 0x70027000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
RuntimeEntityKey key = canonical.Key!.Value;
|
|
|
|
// Directly plant a stale Progress entry under a LeaseId that does
|
|
// NOT match the current lease's token - the exact "an old
|
|
// incarnation's progress leaking into a reused key" scenario the
|
|
// contract requires Execute to discard and fail closed on for this
|
|
// one call, never silently reusing it.
|
|
PlantStaleProgress(lifetime, key, lease.Token.LeaseId + 1_000UL);
|
|
Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedAuthority,
|
|
lifetime.InitialCreateExecution.Execute(canonical, lease.Token, NoContact, out _));
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
Assert.NotEmpty(receipt.Trace);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
private static void PlantStaleProgress(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
RuntimeEntityKey key,
|
|
ulong staleLeaseId)
|
|
{
|
|
Type progressType = typeof(RuntimeInitialCreateContinuationExecutor)
|
|
.GetNestedType("Progress", System.Reflection.BindingFlags.NonPublic)!;
|
|
object stale = System.Runtime.CompilerServices.RuntimeHelpers
|
|
.GetUninitializedObject(progressType);
|
|
System.Reflection.PropertyInfo leaseIdProperty = progressType.GetProperty(
|
|
"LeaseId",
|
|
System.Reflection.BindingFlags.Instance
|
|
| System.Reflection.BindingFlags.Public
|
|
| System.Reflection.BindingFlags.NonPublic)!;
|
|
leaseIdProperty.SetValue(stale, staleLeaseId);
|
|
System.Reflection.FieldInfo progressField = typeof(RuntimeInitialCreateContinuationExecutor)
|
|
.GetField(
|
|
"_progress",
|
|
System.Reflection.BindingFlags.Instance
|
|
| System.Reflection.BindingFlags.NonPublic)!;
|
|
var dictionary = (System.Collections.IDictionary)progressField.GetValue(
|
|
lifetime.InitialCreateExecution)!;
|
|
dictionary[key] = stale;
|
|
}
|
|
|
|
// Round 3 H (saturation): adoption Revision pinned at ulong.MaxValue -
|
|
// CanEnqueue's completed-entry branch requires
|
|
// "Revision < ulong.MaxValue", so a NEW continuation fails closed BEFORE
|
|
// any wire consumption; whatever was ALREADY committed before saturation
|
|
// still drains and converges normally through the executor.
|
|
[Fact]
|
|
public void AdoptionRevisionSaturationFailsClosedBeforeAnyNewContinuationCanEnqueue()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 70UL);
|
|
const uint guid = 0x70032000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
var firstVector = new VectorUpdate.Parsed(
|
|
guid, Vector3.One, Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
|
|
Assert.True(lifetime.TryApplyVector(firstVector, null, out _));
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateResidenceCompletionStatus.Completed,
|
|
lifetime.InitialCreateResidences.Complete(canonical, lease.Token, out _));
|
|
SetCompletedAdoptionRevision(lifetime, canonical.Key!.Value, ulong.MaxValue);
|
|
|
|
var secondVector = new VectorUpdate.Parsed(
|
|
guid, new Vector3(2f, 2f, 2f), Vector3.Zero, InstanceSequence: 1, VectorSequence: 3);
|
|
Assert.False(lifetime.TryApplyVector(secondVector, null, out _));
|
|
Assert.True(lifetime.InitialCreateResidences.TryGetTransaction(
|
|
canonical, out RuntimeInitialCreateResidenceLease afterFailedEnqueue));
|
|
Assert.Single(afterFailedEnqueue.Continuations);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
// Only the ONE continuation committed before saturation ever drains -
|
|
// exactly once, never duplicated by the failed second attempt.
|
|
Assert.Equal(
|
|
[
|
|
RuntimeInitialCreateExecutedActionKind.InitialAdoption,
|
|
RuntimeInitialCreateExecutedActionKind.Vector,
|
|
],
|
|
receipt.Trace.Select(static a => a.Kind));
|
|
Assert.Equal(Vector3.One, canonical.Snapshot.Physics!.Value.Velocity);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Local copy of RuntimeInitialCreateResidenceStateTests'
|
|
/// SetCompletedAdoptionRevision reflection helper (per this task's
|
|
/// explicit instruction to reuse the pattern locally rather than share
|
|
/// test-project internals across files).
|
|
/// </summary>
|
|
private static void SetCompletedAdoptionRevision(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
RuntimeEntityKey key,
|
|
ulong revision)
|
|
{
|
|
Type stateType = typeof(RuntimeInitialCreateResidenceState);
|
|
System.Reflection.FieldInfo completedField = stateType.GetField(
|
|
"_completed",
|
|
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!;
|
|
var completed = (System.Collections.IDictionary)completedField.GetValue(
|
|
lifetime.InitialCreateResidences)!;
|
|
object entry = completed[key]!;
|
|
System.Reflection.PropertyInfo receiptProperty = entry.GetType().GetProperty(
|
|
"Receipt",
|
|
System.Reflection.BindingFlags.Instance
|
|
| System.Reflection.BindingFlags.Public
|
|
| System.Reflection.BindingFlags.NonPublic)!;
|
|
var receipt = (RuntimeInitialCreateResidenceReceipt)receiptProperty.GetValue(entry)!;
|
|
receiptProperty.SetValue(
|
|
entry,
|
|
receipt with { Adoption = receipt.Adoption with { Revision = revision } });
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// External-race detection on the executor's staleness baseline
|
|
// (regression coverage for the narrowed pre-Complete
|
|
// AdvanceExecutorBaseline guard - Finding 1)
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void ExternalPositionAuthorityMutationWithNoPendingPlacementFailsClosedAndConverges()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 16UL);
|
|
const uint guid = 0x70029000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
// Drive the residence directly to "completed + adopted" - the exact
|
|
// state RunInitialTail leaves behind before draining any
|
|
// continuation, with NO pending continuation placement. This is
|
|
// the only state in which the narrowed pre-Complete
|
|
// AdvanceExecutorBaseline guard does NOT resync the baseline
|
|
// before the next Complete() call runs.
|
|
Assert.Equal(
|
|
RuntimeInitialCreateResidenceCompletionStatus.Completed,
|
|
lifetime.InitialCreateResidences.Complete(canonical, lease.Token, out _));
|
|
Assert.True(lifetime.InitialCreateResidences.AdoptCompletedPlacement(canonical, lease.Token));
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
using IDisposable subscription = lifetime.Events.Subscribe(
|
|
new EntityObserver(delta => observed.Add(delta.Change)));
|
|
|
|
// External, non-executor mutation of one of the four baseline
|
|
// fields (never through AdvanceExecutorBaseline) - simulating a
|
|
// genuine race between Execute calls with nothing in flight. Before
|
|
// Finding 1's fix, the OLD unconditional pre-Complete rebaseline
|
|
// would have silently absorbed this and returned Completed instead.
|
|
lifetime.Entities.AdvancePositionAuthority(canonical);
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedAuthority,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
|
|
Assert.Equal(default, receipt);
|
|
Assert.Empty(observed);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExternalFullCellMutationWithNoPendingPlacementFailsClosedAndConverges()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = new();
|
|
Bind(lifetime, 17UL);
|
|
const uint guid = 0x70029100u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateResidenceCompletionStatus.Completed,
|
|
lifetime.InitialCreateResidences.Complete(canonical, lease.Token, out _));
|
|
Assert.True(lifetime.InitialCreateResidences.AdoptCompletedPlacement(canonical, lease.Token));
|
|
|
|
var observed = new List<RuntimeEntityChange>();
|
|
using IDisposable subscription = lifetime.Events.Subscribe(
|
|
new EntityObserver(delta => observed.Add(delta.Change)));
|
|
|
|
// FullCellId is the second field the reviewer explicitly named -
|
|
// exercise it independently of PositionAuthorityVersion.
|
|
lifetime.Entities.SetFullCell(canonical, Cell, Landblock);
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedAuthority,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
|
|
Assert.Equal(default, receipt);
|
|
Assert.Empty(observed);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
}
|
|
|
|
// Round 4 R4-2 / R4-9(b): the reviewer's exact scenario - external
|
|
// SetFullCell DURING the AwaitingContinuationPlacement window (after the
|
|
// continuation's OWN placement has been acknowledged but before the
|
|
// executor consumes it), not merely with no placement in flight at all
|
|
// (that is the PRECEDING test above). Before the fix, both failure arms
|
|
// in ResumePendingPlacement cleared PendingContinuationPlacement and
|
|
// abandoned WITHOUT ever forgetting the retained acknowledged-placement
|
|
// entry - HasRetainedCompletion for this key would then stay true
|
|
// forever, blocking EVERY later placement begin (the runtime-surface.md
|
|
// 3.1 deadlock this executor exists to resolve).
|
|
[Fact]
|
|
public void ExternalFullCellMutationDuringAwaitingContinuationPlacementForgetsThePendingPlacementAndAllowsAFreshOneToBegin()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 90UL);
|
|
const uint guid = 0x70034000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 1, forcePositionSequence: 0, positionX: 40f);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: true, null, null, true, null, out _, out _, out _));
|
|
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out _);
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
|
|
RuntimeEntityKey key = canonical.Key!.Value;
|
|
Assert.True(lifetime.InitialCreateExecution
|
|
.TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
|
|
// Drives the continuation's own placement through prepare/submit/
|
|
// acknowledge - the acknowledged projection is now retained under
|
|
// this exact token, ready for the NEXT Execute call to consume.
|
|
CompletePendingContinuationPlacement(lifetime, key, route);
|
|
|
|
// External mutation of the record's cell AFTER the continuation's
|
|
// own placement was acknowledged but BEFORE the executor consumes
|
|
// it - ResumePendingPlacement's projection/record agreement check
|
|
// must catch this exactly like Complete() does for the initial
|
|
// placement.
|
|
lifetime.Entities.SetFullCell(canonical, canonical.FullCellId + 999u, Landblock);
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.RejectedAuthority,
|
|
lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out RuntimeInitialCreateExecutionReceipt receipt));
|
|
Assert.Equal(default, receipt);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
RuntimeSetPositionOwnershipSnapshot physicsOwnership =
|
|
lifetime.Physics.SetPosition.CaptureOwnership();
|
|
Assert.Equal(0, physicsOwnership.ActiveOperationCount);
|
|
// The central R4-2 claim: no retained-completion leak.
|
|
Assert.Equal(0, physicsOwnership.AcknowledgedPlacementCompletionCount);
|
|
|
|
// A subsequent FRESH authored placement for the SAME entity can
|
|
// begin - proves HasRetainedCompletion no longer blocks it.
|
|
RuntimeEntityPlacementToken fresh = lifetime.Physics.SetPosition
|
|
.TryBeginExclusiveAuthoredPlacement(
|
|
canonical,
|
|
canonical.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.LocalAuthoritative);
|
|
Assert.True(fresh.IsValid);
|
|
}
|
|
|
|
// Round 4 R4-9(a): guards B3's residence-retirement notification edge
|
|
// for a placement STILL IN FLIGHT (not yet acknowledged) - a THIRD
|
|
// PARTY (not the executor) discovers residence staleness through the
|
|
// residence's own host-facing TryGetTransaction query, which internally
|
|
// Retires the completed entry. That Retire must notify the executor so
|
|
// it forgets its OWN separately-tracked pending continuation placement
|
|
// token, not just the residence's initial-lease placement.
|
|
[Fact]
|
|
public void ThirdPartyTryGetTransactionRetireWhileAwaitingContinuationPlacementDiscardsExecutorProgressAndForgetsThePendingPlacement()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 91UL);
|
|
const uint guid = 0x70034100u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 1, forcePositionSequence: 0, positionX: 40f);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: true, null, null, true, null, out _, out _, out _));
|
|
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out _);
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
|
|
Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
RuntimeEntityKey key = canonical.Key!.Value;
|
|
Assert.True(lifetime.InitialCreateExecution
|
|
.TryGetPendingContinuationPlacement(key, out RuntimeEntityPlacementToken pendingPlacement));
|
|
Assert.True(lifetime.Physics.SetPosition.IsPlacementCurrent(pendingPlacement));
|
|
|
|
// External, non-executor mutation of one of the four executor-
|
|
// tracked baseline fields - simulates something OTHER than the
|
|
// executor discovering staleness through a THIRD-PARTY call to the
|
|
// residence's own host-facing TryGetTransaction, never through
|
|
// Execute at all.
|
|
lifetime.Entities.AdvancePositionAuthority(canonical);
|
|
|
|
Assert.False(lifetime.InitialCreateResidences.TryGetTransaction(canonical, out _));
|
|
|
|
// B3's notification edge: the residence's own Retire (triggered by
|
|
// a THIRD PARTY, not the executor) must notify the executor so it
|
|
// forgets the pending continuation placement, not just the
|
|
// residence's own initial-lease placement.
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.False(lifetime.Physics.SetPosition.IsPlacementCurrent(pendingPlacement));
|
|
RuntimeSetPositionOwnershipSnapshot physicsOwnership =
|
|
lifetime.Physics.SetPosition.CaptureOwnership();
|
|
Assert.Equal(0, physicsOwnership.ActiveOperationCount);
|
|
Assert.Equal(0, physicsOwnership.PlacementCompletionWatchCount);
|
|
Assert.Equal(0, physicsOwnership.AcknowledgedPlacementCompletionCount);
|
|
|
|
// A subsequent FRESH authored placement for the SAME entity can
|
|
// begin - no lingering block from the forgotten pending placement.
|
|
RuntimeEntityPlacementToken fresh = lifetime.Physics.SetPosition
|
|
.TryBeginExclusiveAuthoredPlacement(
|
|
canonical,
|
|
canonical.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.LocalAuthoritative);
|
|
Assert.True(fresh.IsValid);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// I. Ownership convergence
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void ResetDuringAwaitingContinuationPlacementConvergesEveryLedger()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 14UL);
|
|
const uint guid = 0x70028000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 1, forcePositionSequence: 0, positionX: 40f);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: true, null, null, true, null, out _, out _, out _));
|
|
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, NoContact, out _);
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
|
|
Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
|
|
IReadOnlyList<RuntimeEntityRecord> retirements = lifetime.BeginSessionClear();
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
foreach (RuntimeEntityRecord record in retirements)
|
|
lifetime.CompleteSessionEntityRetirement(record);
|
|
Assert.True(lifetime.CompleteSessionClearIfConverged());
|
|
}
|
|
|
|
[Fact]
|
|
public void DisposalConvergesTheCompleteOwnershipLedgerAfterASuccessfulExecution()
|
|
{
|
|
var lifetime = new RuntimeEntityObjectLifetime();
|
|
Bind(lifetime, 15UL);
|
|
const uint guid = 0x70028100u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(guid, 1, includePosition: false),
|
|
isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
_ = RunToCompletion(lifetime, canonical, lease.Token, NoContact);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
|
|
lifetime.Dispose();
|
|
|
|
Assert.True(lifetime.CaptureOwnership().IsConverged);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// J. Gap closures (B1 operation-slot contention).
|
|
// ---------------------------------------------------------------
|
|
|
|
// Round 3 B1-gap: a Position continuation's own placement-begin can fail
|
|
// on TRANSIENT operation-slot contention (another operation already owns
|
|
// this entity's SetPosition slot) rather than genuine staleness. The
|
|
// merge+publish must already have committed by that point (this is NOT
|
|
// an abandonment); a retry after the slot frees must complete WITHOUT
|
|
// re-running the merge or re-publishing.
|
|
[Fact]
|
|
public void OperationSlotContentionYieldsRetryableWithoutAbandoningThenRetryCompletesWithNoDuplicatePublish()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 80UL);
|
|
const uint guid = 0x70033000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical, out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
Assert.Equal(Cell, canonical.FullCellId);
|
|
|
|
// Entry 1: a Vector continuation whose own publish is the injection
|
|
// point for occupying the entity's SetPosition slot from OUTSIDE the
|
|
// executor - by the time this fires, RunInitialTail's
|
|
// AdoptCompletedPlacement has already freed the slot the INITIAL
|
|
// placement held, so an external begin here genuinely succeeds.
|
|
var vector = new VectorUpdate.Parsed(
|
|
guid, Vector3.One, Vector3.Zero, InstanceSequence: 1, VectorSequence: 2);
|
|
Assert.True(lifetime.TryApplyVector(vector, null, out _));
|
|
|
|
// Entry 2: a far-distance Remote Position continuation that will
|
|
// require its OWN real SetPosition placement.
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 0, forcePositionSequence: 0, positionX: 25f);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: false, null, null, false, null,
|
|
out PositionTimestampDisposition disposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
|
|
RuntimeEntityPlacementToken unrelatedOccupant = default;
|
|
var observed = new List<RuntimeEntityChange>();
|
|
using IDisposable subscription = lifetime.Events.Subscribe(new EntityObserver(delta =>
|
|
{
|
|
observed.Add(delta.Change);
|
|
if (unrelatedOccupant.IsValid || delta.Change is not RuntimeEntityChange.Updated)
|
|
return;
|
|
unrelatedOccupant = lifetime.Physics.SetPosition.TryBeginExclusiveAuthoredPlacement(
|
|
canonical,
|
|
canonical.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.RemoteAuthoritative);
|
|
Assert.True(unrelatedOccupant.IsValid);
|
|
}));
|
|
|
|
var inputs = new RuntimeInitialCreateExecutionInputs(
|
|
UsePositionFromServer: false, PlayerDistance: 200f);
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, inputs, out RuntimeInitialCreateExecutionReceipt receipt);
|
|
|
|
// Contention, not staleness: retryable, residence/progress both
|
|
// still open (no abandonment).
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, status);
|
|
Assert.Equal(default, receipt);
|
|
Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(1, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
// The merge+publish for BOTH entries already committed before the
|
|
// contention was even discovered.
|
|
Assert.Equal(
|
|
[RuntimeEntityChange.Updated, RuntimeEntityChange.Updated],
|
|
observed);
|
|
Assert.Equal(25f, canonical.Snapshot.Position!.Value.PositionX);
|
|
|
|
// Free the slot; retry.
|
|
lifetime.Physics.SetPosition.PublishCancellation(
|
|
lifetime.Physics.SetPosition.ForgetExactPlacement(unrelatedOccupant));
|
|
|
|
RuntimeInitialCreateExecutionStatus retryStatus = lifetime.InitialCreateExecution.Execute(
|
|
canonical, lease.Token, inputs, out RuntimeInitialCreateExecutionReceipt retryReceipt);
|
|
Assert.Equal(RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement, retryStatus);
|
|
Assert.Equal(default, retryReceipt);
|
|
// No re-merge, no re-publish on the contention retry itself.
|
|
Assert.Equal(
|
|
[RuntimeEntityChange.Updated, RuntimeEntityChange.Updated],
|
|
observed);
|
|
|
|
RuntimeEntityKey key = canonical.Key!.Value;
|
|
Assert.True(lifetime.InitialCreateExecution
|
|
.TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
|
|
Assert.Equal(RuntimeAuthoritativePositionDisposition.SetPositionSimple, route.Disposition);
|
|
CompletePendingContinuationPlacement(lifetime, key, route);
|
|
|
|
RuntimeInitialCreateExecutionReceipt finalReceipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, inputs);
|
|
|
|
Assert.Equal(
|
|
[
|
|
RuntimeInitialCreateExecutedActionKind.InitialAdoption,
|
|
RuntimeInitialCreateExecutedActionKind.Vector,
|
|
RuntimeInitialCreateExecutedActionKind.Position,
|
|
],
|
|
finalReceipt.Trace.Select(static a => a.Kind));
|
|
// Still exactly two publishes across the entire contention +
|
|
// retry + completion sequence.
|
|
Assert.Equal(
|
|
[RuntimeEntityChange.Updated, RuntimeEntityChange.Updated],
|
|
observed);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateResidenceLeaseCount);
|
|
Assert.Equal(0, lifetime.CaptureOwnership().InitialCreateExecutorProgressCount);
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.CaptureOwnership().ActiveOperationCount);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// F. C0-1: executor-completion bridge / C0-2: live-input binding
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void ExecutorCompletion_PublishesOnTheSamePlacementStreamCorrelatedWithTheFullReceipt()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 400UL);
|
|
const uint guid = 0x70024000u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
var observed = new List<RuntimePlacementProjectionSnapshot>();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(
|
|
new PlacementObserver(delta => observed.Add(delta.Placement)));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
RuntimePlacementProjectionSnapshot completion = Assert.Single(observed);
|
|
Assert.Equal(
|
|
RuntimePlacementProjectionKind.ExecutorCompleted,
|
|
completion.Kind);
|
|
Assert.Equal(canonical.Key, completion.Token.Entity);
|
|
Assert.True(lifetime.InitialCreateExecution.TryGetCompletionReceipt(
|
|
completion.Token,
|
|
out RuntimeInitialCreateExecutionReceipt correlated));
|
|
Assert.Equal(receipt, correlated);
|
|
|
|
// Acknowledge-only, exact-head, same as every other Kind.
|
|
Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
completion.Token));
|
|
Assert.Equal(0, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExecutorCompletion_ObservedOnlyAfterAnyContinuationPlacementInFifoOrder()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 401UL);
|
|
const uint guid = 0x70024001u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
// A teleport-advanced Position continuation performs its OWN
|
|
// authored SetPosition - the continuation placement C0-1's contract
|
|
// says already flows through the channel unchanged.
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 1,
|
|
forcePositionSequence: 0, positionX: 40f);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: true, null, null, true, null,
|
|
out PositionTimestampDisposition disposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
|
|
var observed = new List<RuntimePlacementProjectionSnapshot>();
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(
|
|
new PlacementObserver(delta => observed.Add(delta.Placement)));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Equal(
|
|
[
|
|
RuntimePlacementProjectionKind.Place,
|
|
RuntimePlacementProjectionKind.ExecutorCompleted,
|
|
],
|
|
observed.Select(static s => s.Kind));
|
|
Assert.Equal(canonical.Key, observed[0].Token.Entity);
|
|
Assert.True(lifetime.InitialCreateExecution.TryGetCompletionReceipt(
|
|
observed[1].Token,
|
|
out RuntimeInitialCreateExecutionReceipt correlated));
|
|
Assert.Equal(receipt, correlated);
|
|
// The continuation Place receipt was already acknowledged by
|
|
// RunToCompletion's own CompletePendingContinuationPlacement helper
|
|
// before Execute ever reached Completed - only the fresh
|
|
// ExecutorCompleted receipt is still outstanding.
|
|
Assert.Equal(1, lifetime.Physics.SetPosition.PendingProjectionCount);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
observed[1].Token));
|
|
}
|
|
|
|
[Fact]
|
|
public void ExecutorCompletion_ReceiptIsReadableFromWithinTheSameSynchronousOnPlacementDispatch()
|
|
{
|
|
// F2: registration must happen BEFORE PublishPlacement's synchronous
|
|
// observer dispatch - a subscriber reading the correlation back from
|
|
// inside its OWN OnPlacement callback must already find it, not only
|
|
// after RunToCompletion returns.
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 410UL);
|
|
const uint guid = 0x70024010u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
RuntimeInitialCreateExecutionReceipt? observedFromInsideDispatch = null;
|
|
using IDisposable subscription = lifetime.Events.SubscribePlacement(
|
|
new PlacementObserver(delta =>
|
|
{
|
|
if (delta.Placement.Kind
|
|
is not RuntimePlacementProjectionKind.ExecutorCompleted)
|
|
{
|
|
return;
|
|
}
|
|
Assert.True(lifetime.InitialCreateExecution.TryGetCompletionReceipt(
|
|
delta.Placement.Token,
|
|
out RuntimeInitialCreateExecutionReceipt receipt));
|
|
observedFromInsideDispatch = receipt;
|
|
}));
|
|
|
|
RuntimeInitialCreateExecutionReceipt receiptReturned = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.NotNull(observedFromInsideDispatch);
|
|
Assert.Equal(receiptReturned, observedFromInsideDispatch!.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExecutorCompletion_ConvergenceLedgerCountsAnUnacknowledgedReceiptAsOutstandingDebtUntilAcknowledged()
|
|
{
|
|
// F2: PendingCompletionReceiptCount mirrors
|
|
// RuntimeSetPositionOwnershipSnapshot.PendingProjectionAcknowledgementCount's
|
|
// existing "unacknowledged receipt is outstanding debt" shape for
|
|
// the SAME underlying receipt stream - non-zero while unacknowledged,
|
|
// reaped to zero exactly on acknowledge (never before, never left
|
|
// dangling after).
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 411UL);
|
|
const uint guid = 0x70024011u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
Assert.Equal(
|
|
0,
|
|
lifetime.CaptureOwnership().PendingCompletionReceiptCount);
|
|
|
|
RunToCompletion(lifetime, canonical, lease.Token, NoContact);
|
|
|
|
Assert.Equal(
|
|
1,
|
|
lifetime.CaptureOwnership().PendingCompletionReceiptCount);
|
|
Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot completion));
|
|
Assert.Equal(
|
|
RuntimePlacementProjectionKind.ExecutorCompleted,
|
|
completion.Kind);
|
|
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
completion.Token));
|
|
|
|
Assert.Equal(
|
|
0,
|
|
lifetime.CaptureOwnership().PendingCompletionReceiptCount);
|
|
Assert.False(lifetime.InitialCreateExecution.TryGetCompletionReceipt(
|
|
completion.Token,
|
|
out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void ExecutorCompletion_CorrelationEntryIsReapedByDiscardProgress()
|
|
{
|
|
// F2: DiscardProgress (reached via ForgetInitialCreateResidence in
|
|
// production) must reap this correlation cache too - it is exactly
|
|
// the kind of executor-introduced state that method already owns
|
|
// cleaning up. The drain already removed _progress[key] before
|
|
// publishing the completion (see ExecuteCore's Released case), so
|
|
// this proves DiscardProgress reaps _completionReceipts
|
|
// UNCONDITIONALLY, not only when _progress still tracks the key.
|
|
// Left deliberately UNACKNOWLEDGED in _pendingProjection (a single-
|
|
// entity lifetime, so there is no exact-head contention to worry
|
|
// about) - proving DiscardProgress reaps the correlation cache
|
|
// independently of the normal acknowledge path.
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 413UL);
|
|
const uint guid = 0x70024013u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
RunToCompletion(lifetime, canonical, lease.Token, NoContact);
|
|
Assert.Equal(
|
|
1,
|
|
lifetime.CaptureOwnership().PendingCompletionReceiptCount);
|
|
|
|
lifetime.InitialCreateExecution.DiscardProgress(canonical.Key!.Value);
|
|
|
|
Assert.Equal(
|
|
0,
|
|
lifetime.CaptureOwnership().PendingCompletionReceiptCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExecutorCompletion_CorrelationEntryIsReapedByDiscardAll()
|
|
{
|
|
// F2: a full session clear (DiscardAll, reached via
|
|
// RuntimeInitialCreateResidenceState.Clear's call site) must never
|
|
// carry this correlation cache across a reset.
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 414UL);
|
|
const uint guid = 0x70024014u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
RunToCompletion(lifetime, canonical, lease.Token, NoContact);
|
|
Assert.Equal(
|
|
1,
|
|
lifetime.CaptureOwnership().PendingCompletionReceiptCount);
|
|
|
|
lifetime.InitialCreateExecution.DiscardAll();
|
|
|
|
Assert.Equal(
|
|
0,
|
|
lifetime.CaptureOwnership().PendingCompletionReceiptCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void BindLiveInputs_DrivesClassificationFromTheBoundSourcesInsteadOfTheCallerStruct()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 402UL);
|
|
const uint guid = 0x70024002u;
|
|
RuntimeEntityRecord canonical = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(lifetime, canonical);
|
|
CompleteInitialPlacement(lifetime, lease);
|
|
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 0,
|
|
forcePositionSequence: 0, positionX: 15f, isGrounded: true);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: true, null, null, false, null,
|
|
out PositionTimestampDisposition disposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
|
|
bool usePositionFromServer = true;
|
|
// Position 15 units from a local player parked far away (100 units)
|
|
// is irrelevant here (PlayerDistance only matters for the
|
|
// Remote/Projectile near/far branch, not LocalPlayer's own
|
|
// interpolate gate) - it exists purely to prove the DISTANCE source
|
|
// is read at all.
|
|
lifetime.InitialCreateExecution.BindLiveInputs(
|
|
() => usePositionFromServer,
|
|
() => new Vector3(115f, 20f, 7f));
|
|
|
|
// The caller-supplied struct says UsePositionFromServer:false - if
|
|
// the bound source is actually driving classification, retail's
|
|
// "UsePositionFromServer && wire-contact" local-ordinary gate must
|
|
// still interpolate.
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
lifetime, canonical, lease.Token, NoContact);
|
|
RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.Interpolate,
|
|
positionAction.PositionDisposition);
|
|
|
|
// C0-1: Completing the first entity's drain also published its own
|
|
// ExecutorCompleted receipt on the SAME exact-head stream - it must
|
|
// be acknowledged before a SECOND entity's own placement receipt can
|
|
// ever become the head.
|
|
Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot firstCompletion));
|
|
Assert.Equal(
|
|
RuntimePlacementProjectionKind.ExecutorCompleted,
|
|
firstCompletion.Kind);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
firstCompletion.Token));
|
|
|
|
// Flip the bound source off and confirm the SAME struct now takes
|
|
// the non-interpolating branch - proves it is read live, not cached
|
|
// at bind time.
|
|
usePositionFromServer = false;
|
|
const uint secondGuid = 0x70024003u;
|
|
RuntimeEntityRecord second = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(secondGuid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
second,
|
|
out RuntimeInitialCreateResidenceLease secondLease));
|
|
AttachDormantBody(lifetime, second);
|
|
CompleteInitialPlacement(lifetime, secondLease);
|
|
WorldSession.EntityPositionUpdate secondUpdate = PositionUpdate(
|
|
secondGuid, positionSequence: 2, teleportSequence: 0,
|
|
forcePositionSequence: 0, positionX: 16f, isGrounded: true);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
secondUpdate, isLocalPlayer: true, null, null, false, null,
|
|
out PositionTimestampDisposition secondDisposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, secondDisposition);
|
|
RuntimeInitialCreateExecutionReceipt secondReceipt = RunToCompletion(
|
|
lifetime, second, secondLease.Token, NoContact);
|
|
RuntimeInitialCreateExecutedAction secondPositionAction = Assert.Single(
|
|
secondReceipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.NoPositionOperation,
|
|
secondPositionAction.PositionDisposition);
|
|
}
|
|
|
|
[Fact]
|
|
public void BindLiveInputs_ThrowsOnASecondBindAndUnboundExecutorsUseTheCallerStructUnchanged()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
lifetime.InitialCreateExecution.BindLiveInputs(
|
|
static () => true, static () => Vector3.Zero);
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
lifetime.InitialCreateExecution.BindLiveInputs(
|
|
static () => false, static () => Vector3.Zero));
|
|
|
|
// A SEPARATE, never-bound lifetime still honors the caller-supplied
|
|
// struct verbatim - the existing bare-lifetime test contract is
|
|
// unchanged by this slice.
|
|
using RuntimeEntityObjectLifetime unbound = EngineLifetime();
|
|
Bind(unbound, 403UL);
|
|
const uint guid = 0x70024004u;
|
|
RuntimeEntityRecord canonical = unbound
|
|
.RegisterEntityWithInitialResidence(Spawn(guid, 1), isLocalPlayer: true)
|
|
.Canonical!;
|
|
Assert.True(unbound.TryGetInitialCreateResidence(
|
|
canonical,
|
|
out RuntimeInitialCreateResidenceLease lease));
|
|
AttachDormantBody(unbound, canonical);
|
|
CompleteInitialPlacement(unbound, lease);
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 0,
|
|
forcePositionSequence: 0, positionX: 15f, isGrounded: true);
|
|
Assert.True(unbound.TryApplyPosition(
|
|
update, isLocalPlayer: true, null, null, false, null,
|
|
out PositionTimestampDisposition disposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
|
|
RuntimeInitialCreateExecutionReceipt receipt = RunToCompletion(
|
|
unbound, canonical, lease.Token, NoContact);
|
|
RuntimeInitialCreateExecutedAction positionAction = Assert.Single(
|
|
receipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
// NoContact (UsePositionFromServer:false) -> not interpolated.
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.NoPositionOperation,
|
|
positionAction.PositionDisposition);
|
|
}
|
|
|
|
[Fact]
|
|
public void BindLiveInputs_PlayerDistanceIsReadFromANonNullBoundSourceAndFallsBackToTheCallerStructWhenNull()
|
|
{
|
|
// F3: proves BOTH directions of the nullable local-player-position
|
|
// source. Entity 1: the bound source returns a REAL near position -
|
|
// the caller struct claims a FAR distance (200f), so if the bound
|
|
// source is genuinely read (not ignored), the entity's own near
|
|
// distance must win and classify Interpolate. Entity 2: the SAME
|
|
// bound source now returns null (e.g. the login-window drain before
|
|
// RuntimeLocalPlayerMovementState.Controller exists) - it must fall
|
|
// back to the caller struct's FAR distance exactly like an unbound
|
|
// source would, never fabricate Vector3.Zero (which would compute a
|
|
// small, misleadingly-near distance to the entity's own position and
|
|
// wrongly classify Interpolate instead of the far SetPositionSimple
|
|
// hard-snap).
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 421UL);
|
|
Vector3? boundPosition = new Vector3(30f, 20f, 7f);
|
|
lifetime.InitialCreateExecution.BindLiveInputs(
|
|
static () => false,
|
|
() => boundPosition);
|
|
|
|
const uint nearGuid = 0x70024021u;
|
|
RuntimeEntityRecord near = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(nearGuid, 1), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
near, out RuntimeInitialCreateResidenceLease nearLease));
|
|
AttachDormantBody(lifetime, near);
|
|
CompleteInitialPlacement(lifetime, nearLease);
|
|
WorldSession.EntityPositionUpdate nearUpdate = PositionUpdate(
|
|
nearGuid, positionSequence: 2, teleportSequence: 0,
|
|
forcePositionSequence: 0, positionX: 25f, isGrounded: true);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
nearUpdate, isLocalPlayer: false, null, null, false, null,
|
|
out PositionTimestampDisposition nearDisposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, nearDisposition);
|
|
var farStruct = new RuntimeInitialCreateExecutionInputs(
|
|
UsePositionFromServer: false, PlayerDistance: 200f);
|
|
|
|
RuntimeInitialCreateExecutionReceipt nearReceipt = RunToCompletion(
|
|
lifetime, near, nearLease.Token, farStruct);
|
|
|
|
RuntimeInitialCreateExecutedAction nearAction = Assert.Single(
|
|
nearReceipt.Trace,
|
|
static a => a.Kind is RuntimeInitialCreateExecutedActionKind.Position);
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.Interpolate,
|
|
nearAction.PositionDisposition);
|
|
|
|
// C0-1: the completed near entity published its own ExecutorCompleted
|
|
// receipt on the SAME exact-head stream - acknowledge it before the
|
|
// far entity's own Place receipt can ever become the head.
|
|
Assert.True(lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot nearCompletion));
|
|
Assert.Equal(
|
|
RuntimePlacementProjectionKind.ExecutorCompleted,
|
|
nearCompletion.Kind);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(
|
|
nearCompletion.Token));
|
|
|
|
boundPosition = null;
|
|
const uint farGuid = 0x70024022u;
|
|
RuntimeEntityRecord far = lifetime
|
|
.RegisterEntityWithInitialResidence(Spawn(farGuid, 1), isLocalPlayer: false)
|
|
.Canonical!;
|
|
Assert.True(lifetime.TryGetInitialCreateResidence(
|
|
far, out RuntimeInitialCreateResidenceLease farLease));
|
|
AttachDormantBody(lifetime, far);
|
|
CompleteInitialPlacement(lifetime, farLease);
|
|
WorldSession.EntityPositionUpdate farUpdate = PositionUpdate(
|
|
farGuid, positionSequence: 2, teleportSequence: 0,
|
|
forcePositionSequence: 0, positionX: 25f, isGrounded: true);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
farUpdate, isLocalPlayer: false, null, null, false, null,
|
|
out PositionTimestampDisposition farDisposition, out _, out _));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, farDisposition);
|
|
|
|
RuntimeInitialCreateExecutionStatus farStatus = lifetime
|
|
.InitialCreateExecution.Execute(
|
|
far, farLease.Token, farStruct, out _);
|
|
|
|
Assert.Equal(
|
|
RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement,
|
|
farStatus);
|
|
RuntimeEntityKey farKey = far.Key!.Value;
|
|
Assert.True(lifetime.InitialCreateExecution.TryGetPendingContinuationRoute(
|
|
farKey, out RuntimeAuthoritativePositionRoute farRoute));
|
|
Assert.Equal(
|
|
RuntimeAuthoritativePositionDisposition.SetPositionSimple,
|
|
farRoute.Disposition);
|
|
Assert.True(farRoute.StopInterpolating);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Harness
|
|
// ---------------------------------------------------------------
|
|
|
|
private static RuntimeEntityObjectLifetime EngineLifetime()
|
|
{
|
|
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);
|
|
return new RuntimeEntityObjectLifetime(engine);
|
|
}
|
|
|
|
private static RuntimeInitialCreateExecutionReceipt RunToCompletion(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
RuntimeEntityRecord canonical,
|
|
in RuntimeInitialCreateResidenceToken token,
|
|
RuntimeInitialCreateExecutionInputs inputs,
|
|
int maxSteps = 25)
|
|
{
|
|
for (int step = 0; step < maxSteps; step++)
|
|
{
|
|
RuntimeInitialCreateExecutionStatus status = lifetime.InitialCreateExecution.Execute(
|
|
canonical, token, inputs, out RuntimeInitialCreateExecutionReceipt receipt);
|
|
switch (status)
|
|
{
|
|
case RuntimeInitialCreateExecutionStatus.Completed:
|
|
return receipt;
|
|
case RuntimeInitialCreateExecutionStatus.AwaitingContinuationPlacement:
|
|
{
|
|
RuntimeEntityKey key = canonical.Key!.Value;
|
|
Assert.True(lifetime.InitialCreateExecution
|
|
.TryGetPendingContinuationRoute(key, out RuntimeAuthoritativePositionRoute route));
|
|
CompletePendingContinuationPlacement(lifetime, key, route);
|
|
continue;
|
|
}
|
|
default:
|
|
throw new InvalidOperationException(
|
|
$"RunToCompletion hit unexpected status {status}; drive its precondition explicitly instead.");
|
|
}
|
|
}
|
|
throw new InvalidOperationException("RunToCompletion exceeded its step budget.");
|
|
}
|
|
|
|
private static void CompleteInitialPlacement(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
in RuntimeInitialCreateResidenceLease lease)
|
|
{
|
|
RuntimeSetPositionCommand command = Prepare(
|
|
lifetime,
|
|
lease.Placement,
|
|
lease.Route.OperationKind,
|
|
lease.Route.SetPositionFlags);
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(lease.Placement, command);
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
outcome.Status);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(outcome.Projection));
|
|
}
|
|
|
|
private static void CompletePendingContinuationPlacement(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
RuntimeEntityKey key,
|
|
in RuntimeAuthoritativePositionRoute route)
|
|
{
|
|
Assert.True(lifetime.InitialCreateExecution
|
|
.TryGetPendingContinuationPlacement(key, out RuntimeEntityPlacementToken placement));
|
|
RuntimeSetPositionCommand command = Prepare(
|
|
lifetime, placement, route.OperationKind, route.SetPositionFlags);
|
|
RuntimeSetPositionOutcome outcome = lifetime.Physics.SetPosition
|
|
.SubmitPreparedPlacement(placement, command);
|
|
Assert.Equal(
|
|
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
|
|
outcome.Status);
|
|
Assert.True(lifetime.Physics.SetPosition.AcknowledgeProjection(outcome.Projection));
|
|
}
|
|
|
|
private static RuntimeSetPositionCommand Prepare(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
in RuntimeEntityPlacementToken placement,
|
|
RuntimeSetPositionOperationKind operationKind,
|
|
PhysicsSetPositionFlags flags)
|
|
{
|
|
var preparation = new RuntimeSetPositionMoverPreparation(
|
|
RuntimeSetPositionMoverSetup.ResolvedAbsent,
|
|
operationKind,
|
|
GameTime: 1d,
|
|
PhysicsPlacementClass.Ordinary,
|
|
flags);
|
|
Assert.Equal(
|
|
RuntimeSetPositionMoverPreparationStatus.Prepared,
|
|
lifetime.Physics.SetPosition.PrepareMover(
|
|
placement, preparation, out RuntimeSetPositionCommand command));
|
|
return command;
|
|
}
|
|
|
|
private static void AttachDormantBody(
|
|
RuntimeEntityObjectLifetime lifetime,
|
|
RuntimeEntityRecord canonical,
|
|
bool inContact = false)
|
|
{
|
|
var body = new PhysicsBody
|
|
{
|
|
State = canonical.FinalPhysicsState,
|
|
Orientation = Quaternion.Identity,
|
|
InWorld = false,
|
|
TransientState = inContact ? TransientStateFlags.Contact : TransientStateFlags.None,
|
|
};
|
|
lifetime.Entities.SetPhysicsBody(canonical, body);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The real physics sweep an initial-placement commit runs against the
|
|
/// test landblock decides the body's own contact bit from scratch,
|
|
/// clobbering whatever <see cref="AttachDormantBody"/> set beforehand.
|
|
/// Call this AFTER the placement commits to pin the contact bit a
|
|
/// Position-route test actually needs.
|
|
/// </summary>
|
|
private static void ForceContact(RuntimeEntityRecord canonical, bool inContact)
|
|
{
|
|
if (canonical.PhysicsBody is not { } body)
|
|
return;
|
|
body.TransientState = inContact
|
|
? body.TransientState | TransientStateFlags.Contact
|
|
: body.TransientState & ~TransientStateFlags.Contact;
|
|
}
|
|
|
|
private static void Bind(RuntimeEntityObjectLifetime lifetime, ulong generation)
|
|
{
|
|
var token = new RuntimeGenerationToken(generation);
|
|
lifetime.BindEventContext(() => token, static () => 1UL);
|
|
}
|
|
|
|
private static WorldSession.EntityPositionUpdate PositionUpdate(
|
|
uint guid,
|
|
ushort positionSequence,
|
|
ushort teleportSequence,
|
|
ushort forcePositionSequence,
|
|
float positionX,
|
|
bool isGrounded = true)
|
|
{
|
|
return new WorldSession.EntityPositionUpdate(
|
|
guid,
|
|
new CreateObject.ServerPosition(Cell, positionX, 20f, 7f, 1f, 0f, 0f, 0f),
|
|
new Vector3(positionSequence, 2f, 3f),
|
|
PlacementId: positionSequence,
|
|
IsGrounded: isGrounded,
|
|
InstanceSequence: 1,
|
|
PositionSequence: positionSequence,
|
|
TeleportSequence: teleportSequence,
|
|
ForcePositionSequence: forcePositionSequence);
|
|
}
|
|
|
|
private static WorldSession.EntitySpawn Spawn(
|
|
uint guid,
|
|
ushort incarnation,
|
|
bool includePosition = true,
|
|
uint? parentGuid = null,
|
|
ushort positionSequence = 1,
|
|
float positionX = 10f,
|
|
bool missile = false,
|
|
ushort teleportSequence = 0,
|
|
ushort forcePositionSequence = 0,
|
|
ushort movementSequence = 1,
|
|
ushort serverControlSequence = 1)
|
|
{
|
|
CreateObject.ServerPosition? position = includePosition
|
|
? new CreateObject.ServerPosition(Cell, positionX, 20f, 7f, 1f, 0f, 0f, 0f)
|
|
: null;
|
|
uint rawState = (uint)(PhysicsStateFlags.Gravity
|
|
| (missile ? PhysicsStateFlags.Missile : 0));
|
|
var timestamps = new PhysicsTimestamps(
|
|
Position: positionSequence,
|
|
Movement: movementSequence,
|
|
State: 1,
|
|
Vector: 1,
|
|
Teleport: teleportSequence,
|
|
ServerControlledMove: serverControlSequence,
|
|
ForcePosition: forcePositionSequence,
|
|
ObjDesc: 1,
|
|
Instance: incarnation);
|
|
var physics = new PhysicsSpawnData(
|
|
rawState,
|
|
position,
|
|
Movement: null,
|
|
AnimationFrame: null,
|
|
SetupTableId: null,
|
|
MotionTableId: null,
|
|
SoundTableId: null,
|
|
PhysicsScriptTableId: null,
|
|
Parent: parentGuid is { } parent ? new PhysicsAttachment(parent, 1u) : null,
|
|
Children: null,
|
|
Scale: null,
|
|
Friction: null,
|
|
Elasticity: null,
|
|
Translucency: null,
|
|
Velocity: null,
|
|
Acceleration: null,
|
|
AngularVelocity: null,
|
|
DefaultScriptType: null,
|
|
DefaultScriptIntensity: null,
|
|
timestamps);
|
|
return new WorldSession.EntitySpawn(
|
|
guid,
|
|
position,
|
|
SetupTableId: null,
|
|
Array.Empty<CreateObject.AnimPartChange>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
BasePaletteId: null,
|
|
ObjScale: null,
|
|
Name: "initial-create",
|
|
ItemType: null,
|
|
MotionState: null,
|
|
MotionTableId: null,
|
|
PhysicsState: rawState,
|
|
InstanceSequence: incarnation,
|
|
MovementSequence: timestamps.Movement,
|
|
ServerControlSequence: timestamps.ServerControlledMove,
|
|
PositionSequence: positionSequence,
|
|
ParentGuid: parentGuid,
|
|
ParentLocation: parentGuid is null ? null : 1u,
|
|
Physics: physics);
|
|
}
|
|
|
|
private sealed class EntityObserver(Action<RuntimeEntityDelta> onEntity)
|
|
: IRuntimeEntityObjectObserver
|
|
{
|
|
public void OnEntity(in RuntimeEntityDelta delta) => onEntity(delta);
|
|
public void OnInventory(in RuntimeInventoryDelta delta)
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class PlacementObserver(
|
|
Action<RuntimePlacementDelta> onPlacement)
|
|
: IRuntimePlacementObserver
|
|
{
|
|
public void OnPlacement(in RuntimePlacementDelta delta) =>
|
|
onPlacement(delta);
|
|
}
|
|
}
|