Retail re-cells children when their parent crosses a cell, recursively, to unbounded depth. acdream did it from a RENDER tick, so headless parented children were cell-less forever and the canonical cell had two writers. This slice makes Runtime the sole authority and demotes App's tick to presentation-only. Contract: docs/research/2026-08-04-c4-route-7-contract.md; the research that unblocked it is docs/research/2026-08-04-retail-parent-cell-propagation.md (ca96ea5e). Retail: SetPositionInternal @0x00515330 branches on `this->cell == curr_cell` @0x0051536d; the changed branch reaches change_cell @0x00513390, whose delegates leave_cell @0x00510f50 and enter_cell @0x00510ed0 self-recurse over children and write the FULL identity (add_object @0x00510ee2, objcell_id @0x00510f1e, part-array cell id @0x00510f2b, cell pointer @0x00510f35). change_cell itself has no child loop. THE TRAP, recorded because it nearly shipped: the depth-1 loop @0x0051539c-0x005153d8 is the SAME-CELL fast path (objcell_id and part-array id only, deliberately not the cell pointer), NOT the propagation. An implementer who finds it first concludes "depth-1, id-only" and strands every equipped item at a landblock boundary — the #184 class. The clincher against that reading: update_object @0x00515d10 early-returns on `parent != 0` @0x00515d40, so a child never runs its own physics tick and parent propagation is the ONLY mechanism maintaining its cell. Route 7 performs NO placement (DoPickupEvent @0x00452240 = unset_parent + leave_world; DoParentEvent @0x00452290 = set_parent + SetPlacementFrame), so it arms ConstrainTo nowhere — the leash rule INVERTS relative to routes 2/4/5, and both reviewers confirmed nothing arms. Propagation is an ITERATIVE WORKLIST, not recursion. The first implementation recursed with a depth-64 cap; both reviews independently found the cap left a truncated tail at a stale NON-ZERO cell — permanently unrecoverable, logged only under a probe flag, and on the withdraw path exactly the #184 shape AP-142 clause (a) exists to reject. Shipping a fresh #184 instance inside the slice that fixes stranded children was not acceptable, so the cap was removed rather than tuned. The worklist retires the cap, the constant, its register clause, and the failure mode together. Termination: every record on the stack is already at the target pair, so nothing can be pushed twice and a hostile A->B->A cycle collapses without a visited set. The child write deliberately bypasses the public RuntimeEntityDirectory .SetFullCell and calls the record method directly. This is LOAD-BEARING: the public method re-enters PropagateFullCellToChildren, which opens with _propagationWorklist.Clear() — routing children through it mid-drain would wipe the shared stack and silently drop every unprocessed sibling. Any future side effect added to the public SetFullCell must be mirrored by hand at that call site. Deliberate divergence, recorded not disguised: retail's removal path leaves children with a null cell pointer but a STALE nonzero objcell_id @0x005133c1. acdream does not reproduce it, because FullCellId != 0 is the liveness predicate at 45+ sites — faithful porting would mark dead children live. AP-142 records this; clause (d) records that acdream cannot gate propagation on HasPartArray the way enter_cell gates on part_array @0x00510ed8, because the flag's only writers are graphical and headless never sets it — the reason is Slice J LAYERING, not a semantic difference (retail's part_array is itself a mesh-construction product, single assignment site makeAnimObject @0x0050e930 -> CPartArray::CreateSetup @0x0050e93e). D7 adopts retail's unset_parent-before-leave_world order @0x0045227f -> @0x00452286, applied to BOTH pickup paths including the dormant executor replay. Its inertness was verified by reverting it and finding all 12 propagation tests still green — reported honestly rather than papered over with a manufactured test, and independently confirmed by both reviewers. ClassifyLeaveWorld and its request/cause types are DELETED: retail has no classification here, and method-per-cause IS the retail dispatch shape. Wiring it would have forced a vacuous teleport-sequence predicate with the #307 shape. Two review rounds plus a coordinator-required third pass; 5 MAJORs. One was a handoff failure worth recording: enter_cell's part_array guard was correctly identified as load-bearing by the research, dropped by the contract when it enumerated the writes, and inherited as an omission by the code — a right finding that evaporated across two handoffs with nobody re-reading the source. Another was a test that survived deleting the entire behaviour it claimed to pin, because its assertion read a field written unconditionally one line earlier. NoProjection is structurally unreachable from TickChild (TryResolveExactAttachment performs a strictly stronger form of the same guard one call earlier). Kept as a fail-safe, unit-tested directly, and documented in two places rather than wrapped in a fabricated end-to-end test. Headless regression test — the direct gate for this defect, which FAILED before this work because no code path existed: RuntimeLiveEntitySessionControllerTests .DirectSink_D5_StandaloneParentEventCommitsChildToParentsExactCell. Probe: ACDREAM_PROBE_CHILD_CELL=1 emits [child-cell] lines at all four write sites (attach / headless-attach / propagate / withdraw / delete). TEMPORARY. Complete Release suite MEASURED at 11,079 passed / 4 skipped / 0 failed (baseline 11,063 atcff52c44, +16). An allocation flake appeared once under load and was proven NOT this slice by reachability — RuntimeCollisionReportingState contains zero SetFullCell and zero ParentAttachments references. STILL OWED: the two-client connected gate (equip/unequip, carry across landblock boundaries, pickup, loot, reconnect) with ACDREAM_PROBE_CHILD_CELL=1, and a session counts only if [child-cell] cause=propagate lines appear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5207 lines
259 KiB
C#
5207 lines
259 KiB
C#
using System.Collections.Immutable;
|
|
using System.Numerics;
|
|
using System.Reflection;
|
|
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, 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, 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, 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, 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, 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, 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, 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, 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) - the
|
|
// route-7 contract deleted the classifier's other AwaitFreshPosition
|
|
// producer, ClassifyLeaveWorld, which had zero production callers (see
|
|
// docs/research/2026-08-04-c4-route-7-contract.md D6). 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, 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, 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, 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, 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, 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, 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));
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// C3-1: the public RuntimePlacementProjectionChannel host consumption
|
|
// surface for an executor completion (TryGetInitialCreateCompletion).
|
|
// ---------------------------------------------------------------
|
|
|
|
[Fact]
|
|
public void PlacementChannel_TryGetInitialCreateCompletion_ProjectsHookPhaseCellAndReplayCount()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 420UL);
|
|
const uint guid = 0x70024020u;
|
|
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);
|
|
var generation = new RuntimeGenerationToken(420UL);
|
|
Assert.True(lifetime.Placements.TryGetInitialCreateCompletion(
|
|
generation,
|
|
completion.Token,
|
|
out RuntimeInitialCreatePlacementCompletion publicCompletion));
|
|
Assert.Equal(canonical.Key, publicCompletion.Entity);
|
|
Assert.Equal(receipt.Entity, publicCompletion.Entity);
|
|
Assert.Equal(receipt.FullCellId, publicCompletion.FullCellId);
|
|
Assert.Equal(receipt.ReplayedDeferredChildCount, publicCompletion.ReplayedDeferredChildCount);
|
|
// This is a local-player Create (login): retail's init_player path
|
|
// requests the AfterEnterWorld teleport hook (see RunInitialTail) -
|
|
// the exact fact route-1/8's cutover caller needs to know whether to
|
|
// run the after-enter teleport suffix. No Position continuation ran
|
|
// in this scenario, so the route-fact array projects empty.
|
|
Assert.Equal(
|
|
RuntimeInitialCreateTeleportHookPhase.AfterEnterWorld,
|
|
publicCompletion.TeleportHookPhase);
|
|
Assert.Equal(RuntimeTeleportHookPhase.AfterEnterWorld, receipt.TeleportHookPhase);
|
|
Assert.Empty(publicCompletion.PositionRouteFacts);
|
|
}
|
|
|
|
[Fact]
|
|
public void PlacementChannel_TryGetInitialCreateCompletion_ProjectsPositionRouteFactsForConstrainInterpolationBinding()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 421UL);
|
|
const uint guid = 0x70024021u;
|
|
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 and lands a Position trace entry with real
|
|
// route facts - the same scenario as
|
|
// ExecutorCompletion_ObservedOnlyAfterAnyContinuationPlacementInFifoOrder.
|
|
WorldSession.EntityPositionUpdate update = PositionUpdate(
|
|
guid, positionSequence: 2, teleportSequence: 1,
|
|
forcePositionSequence: 0, positionX: 40f);
|
|
Assert.True(lifetime.TryApplyPosition(
|
|
update, isLocalPlayer: true, null, null, 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);
|
|
|
|
RuntimePlacementProjectionSnapshot executorCompletion = observed[^1];
|
|
Assert.Equal(
|
|
RuntimePlacementProjectionKind.ExecutorCompleted,
|
|
executorCompletion.Kind);
|
|
|
|
RuntimeInitialCreateExecutedAction internalPositionTrace = Assert.Single(
|
|
receipt.Trace.Where(
|
|
static a => a.Kind == RuntimeInitialCreateExecutedActionKind.Position));
|
|
|
|
var generation = new RuntimeGenerationToken(421UL);
|
|
Assert.True(lifetime.Placements.TryGetInitialCreateCompletion(
|
|
generation,
|
|
executorCompletion.Token,
|
|
out RuntimeInitialCreatePlacementCompletion publicCompletion));
|
|
RuntimeInitialCreatePositionRouteFact fact = Assert.Single(
|
|
publicCompletion.PositionRouteFacts);
|
|
Assert.Equal(internalPositionTrace.Sequence, fact.Sequence);
|
|
Assert.Equal(internalPositionTrace.StopInterpolating, fact.StopInterpolating);
|
|
Assert.Equal(internalPositionTrace.ZeroVelocity, fact.ZeroVelocity);
|
|
Assert.Equal(internalPositionTrace.PreserveHeading, fact.PreserveHeading);
|
|
Assert.Equal(
|
|
internalPositionTrace.SendPositionImmediately,
|
|
fact.SendPositionImmediately);
|
|
Assert.Equal(
|
|
internalPositionTrace.PositionDisposition!.Value.ToString(),
|
|
fact.Disposition.ToString());
|
|
Assert.Equal(
|
|
internalPositionTrace.ConstrainPhase.ToString(),
|
|
fact.ConstrainPhase.ToString());
|
|
Assert.Equal(
|
|
internalPositionTrace.HookPhase.ToString(),
|
|
fact.HookPhase.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public void PlacementChannel_TryGetInitialCreateCompletion_RejectsWrongGeneration()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 422UL);
|
|
const uint guid = 0x70024022u;
|
|
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)));
|
|
RunToCompletion(lifetime, canonical, lease.Token, NoContact);
|
|
RuntimePlacementProjectionSnapshot completion = Assert.Single(observed);
|
|
|
|
Assert.False(lifetime.Placements.TryGetInitialCreateCompletion(
|
|
new RuntimeGenerationToken(999UL),
|
|
completion.Token,
|
|
out RuntimeInitialCreatePlacementCompletion stale));
|
|
Assert.Equal(default, stale);
|
|
}
|
|
|
|
[Fact]
|
|
public void PlacementChannel_TryGetInitialCreateCompletion_ReturnsFalseAfterAcknowledgeReapsTheCorrelationEntry()
|
|
{
|
|
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
|
|
Bind(lifetime, 423UL);
|
|
const uint guid = 0x70024023u;
|
|
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.True(lifetime.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot completion));
|
|
var generation = new RuntimeGenerationToken(423UL);
|
|
Assert.True(lifetime.Placements.TryGetInitialCreateCompletion(
|
|
generation,
|
|
completion.Token,
|
|
out _));
|
|
|
|
Assert.True(lifetime.Placements.Acknowledge(generation, completion.Token));
|
|
|
|
Assert.False(lifetime.Placements.TryGetInitialCreateCompletion(
|
|
generation,
|
|
completion.Token,
|
|
out RuntimeInitialCreatePlacementCompletion afterAck));
|
|
Assert.Equal(default, afterAck);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Review fix (2026-08-02): the compiler's exhaustiveness net for the
|
|
/// three enum-mirror switches (<c>MapHookPhase</c>/<c>MapDisposition</c>/
|
|
/// <c>MapConstrainPhase</c>) is gone the moment a catch-all arm exists -
|
|
/// that is exactly why those catch-alls now throw instead of silently
|
|
/// defaulting. This reflection-based test is the runtime replacement:
|
|
/// for each (internal enum, public projection enum, private mapper
|
|
/// method) triple it asserts equal arity AND drives every declared
|
|
/// internal value through the mapper via reflection (the methods are
|
|
/// `private static`), asserting the mapped public value's NAME equals
|
|
/// the internal value's name (every mapper is a literal 1:1 name
|
|
/// mirror by design - see each public enum's own doc comment). Adding a
|
|
/// new member to either enum without updating the other and the mapper
|
|
/// fails this test immediately, mirroring
|
|
/// <c>OperationResetAllFieldsToDefaultTouchesEveryDeclaredField</c>'s
|
|
/// reflection-based completeness guard for
|
|
/// <see cref="RuntimeSetPositionState"/>'s pooled Operation fields.
|
|
/// Sabotage-verified during development: temporarily adding an extra
|
|
/// member to <c>RuntimeTeleportHookPhase</c> (with no matching arm in
|
|
/// <c>MapHookPhase</c> or the public
|
|
/// <see cref="RuntimeInitialCreateTeleportHookPhase"/> enum) failed this
|
|
/// test exactly as predicted - both the arity assertion and the
|
|
/// unhandled-value invocation threw - before the sabotage was reverted.
|
|
/// </summary>
|
|
[Fact]
|
|
public void EnumProjectionMapsHaveEqualArityAndEveryInternalValueRoundTripsByName()
|
|
{
|
|
AssertMapIsCompleteAndNamePreserving(
|
|
typeof(RuntimeTeleportHookPhase),
|
|
typeof(RuntimeInitialCreateTeleportHookPhase),
|
|
"MapHookPhase");
|
|
AssertMapIsCompleteAndNamePreserving(
|
|
typeof(RuntimeAuthoritativePositionDisposition),
|
|
typeof(RuntimeInitialCreatePositionDisposition),
|
|
"MapDisposition");
|
|
AssertMapIsCompleteAndNamePreserving(
|
|
typeof(RuntimePositionConstrainPhase),
|
|
typeof(RuntimeInitialCreatePositionConstrainPhase),
|
|
"MapConstrainPhase");
|
|
}
|
|
|
|
private static void AssertMapIsCompleteAndNamePreserving(
|
|
Type internalEnumType,
|
|
Type publicEnumType,
|
|
string mapMethodName)
|
|
{
|
|
MethodInfo? method = typeof(RuntimeInitialCreateContinuationExecutor)
|
|
.GetMethod(
|
|
mapMethodName,
|
|
BindingFlags.NonPublic | BindingFlags.Static);
|
|
Assert.True(
|
|
method is not null,
|
|
$"{nameof(RuntimeInitialCreateContinuationExecutor)} no longer " +
|
|
$"declares a private static method named {mapMethodName} - " +
|
|
"update this test's reflection lookup to match.");
|
|
|
|
Array internalValues = Enum.GetValues(internalEnumType);
|
|
Array publicValues = Enum.GetValues(publicEnumType);
|
|
// Equal arity: every internal value must have exactly one public
|
|
// counterpart and vice versa. A mismatch here is the first sign
|
|
// either enum grew without the other (or the mapper) being updated
|
|
// to match.
|
|
Assert.True(
|
|
internalValues.Length == publicValues.Length,
|
|
$"{internalEnumType.Name} has {internalValues.Length} values " +
|
|
$"but {publicEnumType.Name} has {publicValues.Length} - keep " +
|
|
"the internal/public enum pair in lockstep.");
|
|
|
|
foreach (object? internalValue in internalValues)
|
|
{
|
|
// Invoked via reflection deliberately - a value this mapper
|
|
// cannot handle now throws ArgumentOutOfRangeException (see
|
|
// the mapper's own doc comment), which TargetInvocationException
|
|
// propagates through Invoke and fails this test with a clear
|
|
// message identifying exactly which enum member is unmapped.
|
|
object? mapped = method!.Invoke(null, [internalValue]);
|
|
Assert.Equal(internalValue!.ToString(), mapped!.ToString());
|
|
}
|
|
}
|
|
|
|
[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, 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, 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, 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, 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, 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);
|
|
}
|
|
}
|