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