acdream/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs
Erik 75ea269d35 feat(physics): S2 chunk 4 - movement publishes from the transition's cells; children inherit at registration
Movement: CommitSetPosition's RefreshPositionRows/ReplacePositionRows and
the staged apply publish the retail render product from the exact cell
list collision just used, the transition's cell_array retail feeds
add_shadows_to_cells in CPhysicsObj::SetPositionInternal @0x00515330
(pseudo-C 283526-283539); the separate move-path bbox recompute is deleted.
calc_cross_cells @0x00515230 stays the distinct full-recompute path
(PhysicsShadowCommitAction.Recalculate).

Children (Contract B recursion): ShadowObjectRegistry.AttachChild/DetachChild
give an attached object the root's current cells as part entries only,
republished whenever the root's array changes, detached at withdrawal and
cascaded from the root's Deregister; nested attachment resolves to the root
with a bounded, cycle-safe chain. EquippedChildRenderController attaches at
realization (FromSetupRenderParts over the child's Setup) and detaches at
its single removal funnel. WalkProductionWorldData's dynamic sweep reads
TryGetRetailCellArray directly; the 64-hop parent-chain walk and its
FindParentLocalId plumbing are deleted. CollisionWorldState.Clear now
also clears the retail products.

Gates (implementer's isolated worktree at identical content): Release
build 0/0; Core 4,970/4,970; App hermetic 6,761/6,761; targeted
walk/child/live-entity/placement/comparator 166/166; Runtime 1,884/1,884.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 23:37:34 +02:00

2030 lines
84 KiB
C#

using System.Collections;
using System.Numerics;
using System.Reflection;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Content;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using AcDream.Runtime.Entities;
using DatReaderWriter.Types;
namespace AcDream.App.Tests.Rendering;
public sealed class EquippedChildProjectionWithdrawalTests
{
[Fact]
public void WithdrawalFailure_DoesNotPublishAttachedProjectionRemoval()
{
bool published = false;
ExactProjectionWithdrawalOutcome outcome =
EquippedChildRenderController.WithdrawAttachedProjection(
ChildRecord(),
positionAuthorityVersion: 1,
projectionMutationVersion: 2,
(_, _, _) => new ExactProjectionWithdrawalOutcome(
ExactProjectionWithdrawalDisposition.Pending,
new InvalidOperationException("injected component cleanup failure")),
() =>
{
published = true;
return true;
});
Assert.IsType<InvalidOperationException>(outcome.Failure);
Assert.Equal(ExactProjectionWithdrawalDisposition.Pending, outcome.Disposition);
Assert.False(published);
}
[Fact]
public void ExactIncarnationRejection_DoesNotPublishAttachedProjectionRemoval()
{
bool published = false;
ExactProjectionWithdrawalOutcome outcome = EquippedChildRenderController.WithdrawAttachedProjection(
ChildRecord(),
positionAuthorityVersion: 1,
projectionMutationVersion: 2,
(_, _, _) => new ExactProjectionWithdrawalOutcome(
ExactProjectionWithdrawalDisposition.Superseded,
Failure: null),
() =>
{
published = true;
return true;
});
Assert.True(published);
Assert.Equal(ExactProjectionWithdrawalDisposition.Superseded, outcome.Disposition);
}
[Fact]
public void SuccessfulWithdrawal_PublishesLocalProjectionRemovalOnce()
{
int publications = 0;
ExactProjectionWithdrawalOutcome outcome = EquippedChildRenderController.WithdrawAttachedProjection(
ChildRecord(),
positionAuthorityVersion: 1,
projectionMutationVersion: 2,
(_, _, _) => new ExactProjectionWithdrawalOutcome(
ExactProjectionWithdrawalDisposition.Completed,
Failure: null),
() =>
{
publications++;
return true;
});
Assert.Equal(ExactProjectionWithdrawalDisposition.Completed, outcome.Disposition);
Assert.Equal(1, publications);
}
[Fact]
public void LogicalReplacement_RemovesExactAttachedMapWithoutWithdrawingReplacement()
{
int withdrawals = 0;
using var fixture = new ControllerFixture(
(_, _, _) =>
{
withdrawals++;
return new(
ExactProjectionWithdrawalDisposition.Completed,
Failure: null);
});
LiveEntityRecord parent = fixture.Spawn(0x70000200u, generation: 1);
LiveEntityRecord oldChild = fixture.Spawn(
0x70000201u,
generation: 1,
LiveEntityProjectionKind.Attached);
fixture.InstallAttached(parent, oldChild);
LiveEntityRecord replacement = fixture.Live.RegisterAndMaterializeProjection(
ControllerFixture.SpawnData(0x70000201u, generation: 2));
Assert.Empty(fixture.Controller.AttachedEntityIds);
Assert.True(fixture.Live.TryGetRecord(0x70000201u, out LiveEntityRecord current));
Assert.Same(replacement, current);
Assert.Equal(0, withdrawals);
}
[Fact]
public void LogicalDelete_NotificationFailureLeavesRetryableTombstoneButNoAttachedLeak()
{
using var fixture = new ControllerFixture((_, _, _) =>
new(ExactProjectionWithdrawalDisposition.Completed, Failure: null));
LiveEntityRecord parent = fixture.Spawn(0x70000210u, generation: 1);
LiveEntityRecord child = fixture.Spawn(
0x70000211u,
generation: 1,
LiveEntityProjectionKind.Attached);
fixture.InstallAttached(parent, child);
fixture.Controller.ProjectionRemoved += Throw;
Assert.Throws<AggregateException>(() => fixture.Live.UnregisterLiveEntity(
new DeleteObject.Parsed(0x70000211u, InstanceSequence: 1),
isLocalPlayer: false));
Assert.Empty(fixture.Controller.AttachedEntityIds);
fixture.Controller.ProjectionRemoved -= Throw;
Assert.Equal(1, fixture.Live.RetryPendingTeardowns());
static void Throw(LiveEntityRecord _) =>
throw new InvalidOperationException("injected projection observer failure");
}
[Fact]
public void PostCommitWithdrawalFailure_RetiresCapturedAttachedMap()
{
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
bool committed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
Assert.True(committed);
return new(
ExactProjectionWithdrawalDisposition.Completed,
new InvalidOperationException("observer failed after commit"));
});
using (fixture)
{
LiveEntityRecord parent = fixture.Spawn(0x70000220u, generation: 1);
LiveEntityRecord child = fixture.Spawn(
0x70000221u,
generation: 1,
LiveEntityProjectionKind.Attached);
fixture.InstallAttached(parent, child);
Assert.Throws<InvalidOperationException>(() =>
fixture.Controller.OnChildBecameUnparented(0x70000221u));
Assert.Empty(fixture.Controller.AttachedEntityIds);
Assert.False(child.IsSpatiallyProjected);
}
}
[Fact]
public void PendingTopLevelWithdrawal_RetryRunsAcceptedContinuationOnce()
{
int attempts = 0;
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
attempts++;
if (attempts == 1)
{
return new(
ExactProjectionWithdrawalDisposition.Pending,
new InvalidOperationException("component cleanup failed"));
}
bool committed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
committed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
LiveEntityRecord record = fixture.Spawn(0x70000230u, generation: 1);
int continuations = 0;
Assert.Throws<InvalidOperationException>(() =>
fixture.Controller.OnChildBecameUnparented(
0x70000230u,
() =>
{
continuations++;
Assert.True(fixture.Live.RebucketLiveEntity(
0x70000230u,
0x01010001u));
}));
fixture.Controller.Tick();
Assert.Equal(2, attempts);
Assert.Equal(1, continuations);
Assert.True(record.IsSpatiallyProjected);
}
}
[Fact]
public void PendingUnparent_NewerPositionSupersedesRecoveryContinuation()
{
using var fixture = new ControllerFixture((_, _, _) =>
new(
ExactProjectionWithdrawalDisposition.Pending,
new InvalidOperationException("component cleanup failed")));
LiveEntityRecord record = fixture.Spawn(0x70000240u, generation: 1);
int continuations = 0;
Assert.Throws<InvalidOperationException>(() =>
fixture.Controller.OnChildBecameUnparented(
0x70000240u,
() => continuations++));
var newer = new WorldSession.EntityPositionUpdate(
0x70000240u,
new CreateObject.ServerPosition(
0x01010001u, 4f, 5f, 6f, 1f, 0f, 0f, 0f),
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 0,
ForcePositionSequence: 0);
Assert.True(fixture.Live.TryApplyPosition(
newer,
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _));
fixture.Controller.Tick();
Assert.Equal(0, continuations);
Assert.True(record.IsSpatiallyProjected);
}
[Fact]
public void PostNetworkReconcile_SkipsStableTree_AndUpdatesChangedBranchParentFirst()
{
using var fixture = new ControllerFixture((_, _, _) =>
new(
ExactProjectionWithdrawalDisposition.Completed,
Failure: null));
LiveEntityRecord root = fixture.Spawn(0x70000241u, generation: 1);
LiveEntityRecord child = fixture.Spawn(
0x70000242u,
generation: 1,
LiveEntityProjectionKind.Attached);
LiveEntityRecord grandchild = fixture.Spawn(
0x70000243u,
generation: 1,
LiveEntityProjectionKind.Attached);
fixture.InstallAttached(root, child);
fixture.InstallAttached(child, grandchild);
fixture.Poses.Publish(root.WorldEntity!, Array.Empty<Matrix4x4>());
fixture.Controller.Tick();
Assert.Equal(2, fixture.Controller.LastFullPoseCompositionVisits);
fixture.Controller.ReconcileSpatialMutations();
Assert.Equal(0, fixture.Controller.LastReconcilePoseCompositionVisits);
root.WorldEntity!.SetPosition(new Vector3(7f, 8f, 9f));
Assert.True(fixture.Poses.UpdateRoot(root.WorldEntity));
fixture.Controller.ReconcileSpatialMutations();
Assert.Equal(2, fixture.Controller.LastReconcilePoseCompositionVisits);
Assert.Equal(root.WorldEntity.Position, child.WorldEntity!.Position);
Assert.Equal(child.WorldEntity.Position, grandchild.WorldEntity!.Position);
}
[Fact]
public void StablePostNetworkReconcile_AllocatesNoTransitionSnapshots()
{
using var fixture = new ControllerFixture((_, _, _) =>
new(
ExactProjectionWithdrawalDisposition.Completed,
Failure: null));
LiveEntityRecord root = fixture.Spawn(0x70000244u, generation: 1);
LiveEntityRecord child = fixture.Spawn(
0x70000245u,
generation: 1,
LiveEntityProjectionKind.Attached);
fixture.InstallAttached(root, child);
fixture.Poses.Publish(root.WorldEntity!, Array.Empty<Matrix4x4>());
fixture.Controller.Tick();
fixture.Controller.ReconcileSpatialMutations();
// #250: the measured window was a 1,000-iteration loop written inline.
ZeroAllocationProbe.AssertAllocatesNothing(
"LiveEntityController.ReconcileSpatialMutations",
() => fixture.Controller.ReconcileSpatialMutations());
Assert.Equal(0, fixture.Controller.LastReconcilePoseCompositionVisits);
}
/// <summary>
/// C4 route 7 D4 (review round 2, A1/R2 remediation): TickChild's
/// rebucket is now presentation-only — Runtime's D1/D2 propagation is
/// the canonical writer. Drives the parent's canonical cell change
/// through a Runtime producer (<c>CommitRebucket</c>, standing in for
/// any of D2's four writer families) BEFORE any render tick runs,
/// proving the canonical half needs no tick at all; then ticks and
/// proves the DRAW BUCKET itself moved — the actual
/// <c>GpuWorldState</c> spatial index membership, not the
/// <c>ParentCellId</c> mirror field (which <c>TickChild</c> writes
/// unconditionally before the rebucket call and therefore proves
/// nothing about it — the original review's A1 finding). Both
/// landblocks are registered so membership is checked by an actual
/// per-landblock query (<c>CopyLiveEntitiesNearLandblock</c>), not by a
/// field read.
/// </summary>
[Fact]
public void TickChild_D4_PresentationBucketMovesToTheDestinationLandblock()
{
using var fixture = new ControllerFixture((_, _, _) =>
new(ExactProjectionWithdrawalDisposition.Completed, Failure: null));
const uint oldLandblock = 0x0101FFFFu;
const uint newCell = 0x01020001u;
const uint newLandblock = 0x0102FFFFu;
fixture.Spatial.AddLandblock(new LoadedLandblock(
newLandblock, new LandBlock(), Array.Empty<WorldEntity>()));
LiveEntityRecord parent = fixture.Spawn(0x70000260u, generation: 1);
LiveEntityRecord child = fixture.Spawn(
0x70000261u,
generation: 1,
LiveEntityProjectionKind.Attached);
fixture.InstallAttached(parent, child);
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
var relation = new ParentAttachmentRelation(
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1);
fixture.CommitRenderedRelation(relation);
// Sanity: the child's draw bucket starts in the OLD landblock (set
// by materialization), not the new one — equality after the tick
// cannot be coincidental.
var buffer = new List<KeyValuePair<uint, WorldEntity>>();
fixture.Spatial.CopyLiveEntitiesNearLandblock(oldLandblock, 0, buffer);
Assert.Contains(buffer, kv => kv.Value == child.WorldEntity);
fixture.Spatial.CopyLiveEntitiesNearLandblock(newLandblock, 0, buffer);
Assert.DoesNotContain(buffer, kv => kv.Value == child.WorldEntity);
// D2: the parent's canonical cell write propagates to the
// committed child's canonical cell with NO render tick involved.
Assert.True(fixture.EntityObjects.CommitRebucket(
parent.Canonical, newCell, newLandblock));
Assert.Equal(newCell, child.Canonical.FullCellId);
ulong childSpatialVersionBeforeTick =
child.Canonical.SpatialAuthorityVersion;
// The parent's OWN presentation already followed its canonical
// cell (a route-7-unrelated concern) so TickChild's pose loop reads
// the same destination; give the child a stale draw bucket first.
parent.WorldEntity!.ParentCellId = newCell;
child.WorldEntity!.ParentCellId = 0x01010001u;
fixture.Controller.Tick();
// D4: the render tick moved the actual spatial bucket — the child
// left the OLD landblock's live-entity set and joined the NEW one.
fixture.Spatial.CopyLiveEntitiesNearLandblock(newLandblock, 0, buffer);
Assert.Contains(buffer, kv => kv.Value == child.WorldEntity);
fixture.Spatial.CopyLiveEntitiesNearLandblock(oldLandblock, 0, buffer);
Assert.DoesNotContain(buffer, kv => kv.Value == child.WorldEntity);
// ...and did not re-write the canonical cell.
Assert.Equal(
childSpatialVersionBeforeTick,
child.Canonical.SpatialAuthorityVersion);
}
/// <summary>
/// C4 route 7 D4 (review round 2, A2/R3 remediation; round-3 review
/// B1/B2/N5 fix): the KNOWN-benign decline. The committed relation can
/// unwind (pickup / Position-unparent) while <c>_attachedByChild</c>
/// still holds the child, ahead of <c>OnChildBecameUnparented</c>'s own
/// teardown. TickChild must not silently discard that disposition and
/// must not move the draw bucket on a write-nothing outcome, but the
/// tick itself still "succeeds" (the pose composed and published)
/// rather than being torn down as a pose-loss failure.
///
/// <para>
/// CORRECTED (B1/B2/N5, round-3 review): the first round's "bucket
/// didn't move" assertion was non-discriminating — the parent's cell
/// never changed in that version, so the child's bucket would have
/// stayed in the same landblock whether or not the NotAttached guard
/// fired at all. This version moves the PARENT's canonical cell to a
/// real second landblock BEFORE removing the relation, so if the guard
/// were bypassed the child WOULD move there — the assertion can now
/// actually fail.
/// </para>
/// </summary>
[Fact]
public void TickChild_D4_NotAttachedDisposition_SkipsTheBucketMoveWithoutFailingTheTick()
{
using var fixture = new ControllerFixture((_, _, _) =>
new(ExactProjectionWithdrawalDisposition.Completed, Failure: null));
const uint oldLandblock = 0x0101FFFFu;
const uint newCell = 0x01020001u;
const uint newLandblock = 0x0102FFFFu;
fixture.Spatial.AddLandblock(new LoadedLandblock(
newLandblock, new LandBlock(), Array.Empty<WorldEntity>()));
LiveEntityRecord parent = fixture.Spawn(0x70000262u, generation: 1);
LiveEntityRecord child = fixture.Spawn(
0x70000263u,
generation: 1,
LiveEntityProjectionKind.Attached);
fixture.InstallAttached(parent, child);
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
var relation = new ParentAttachmentRelation(
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1);
fixture.CommitRenderedRelation(relation);
// A real destination now exists: move the parent's canonical cell
// (D2 propagates it to the still-committed child too) and its
// presentation field, so TickChild's pose loop WOULD send the
// child to newLandblock if the NotAttached guard did not fire.
Assert.True(fixture.EntityObjects.CommitRebucket(
parent.Canonical, newCell, newLandblock));
parent.WorldEntity!.ParentCellId = newCell;
// Runtime already unwound the relation (pickup/Position-unparent),
// but the App's _attachedByChild still holds the child (the
// pre-OnChildBecameUnparented window A2 names).
Assert.True(fixture.Live.ParentAttachments.HasCommittedParent(
child.ServerGuid));
fixture.Live.ParentAttachments.EndChildProjection(child.ServerGuid);
Assert.False(fixture.Live.ParentAttachments.HasCommittedParent(
child.ServerGuid));
int visitsBefore = fixture.Controller.LastFullPoseCompositionVisits;
fixture.Controller.Tick();
// The tick still ran and composed the pose (not treated as pose
// loss)...
Assert.Equal(visitsBefore + 1, fixture.Controller.LastFullPoseCompositionVisits);
Assert.Contains(child.WorldEntity!.Id, fixture.Controller.AttachedEntityIds);
// ...but the draw bucket did NOT move to the new landblock — a real
// destination existed and the guard correctly declined
// (NotAttached), so RebucketLiveEntityPresentationOnly never ran.
var buffer = new List<KeyValuePair<uint, WorldEntity>>();
fixture.Spatial.CopyLiveEntitiesNearLandblock(oldLandblock, 0, buffer);
Assert.Contains(buffer, kv => kv.Value == child.WorldEntity);
fixture.Spatial.CopyLiveEntitiesNearLandblock(newLandblock, 0, buffer);
Assert.DoesNotContain(buffer, kv => kv.Value == child.WorldEntity);
}
/// <summary>
/// C4 route 7 D4 (round-3 review, N6): the OTHER new disposition value
/// — <see cref="EquippedChildPresentationRebucketDisposition.NoProjection"/>.
///
/// <para>
/// CORRECTED (round-3 sabotage pass): the first version of this test
/// called <c>Controller.Tick()</c> and asserted <c>ProjectionPoseReady</c>
/// did not fire, expecting to exercise TickChild's
/// <c>if (disposition is NoProjection) return false;</c> guard at
/// <c>EquippedChildRenderController.cs:419-427</c>. Sabotaging that exact
/// guard (forcing the branch to be skipped) did NOT break the test —
/// investigation showed why: <c>TickChild</c>'s own top-of-method gate,
/// <c>TryResolveExactAttachment</c> (called at both
/// <c>EquippedChildRenderController.cs:379</c> and again at <c>:412</c>
/// immediately before the rebucket call), requires
/// <c>_liveEntities.IsCurrentRecord(child.ChildRecord)</c> to hold — and
/// <c>IsCurrentRecord</c> (<c>LiveEntityRuntime.cs:3336-3338</c>) is
/// itself defined as <c>_projections.TryGetCurrent(guid, out current) &amp;&amp;
/// ReferenceEquals(current, record)</c> — the EXACT SAME store lookup
/// <c>RebucketEquippedChildPresentation</c>'s own <c>NoProjection</c>
/// guard (<c>LiveEntityRuntime.cs:1165</c>) performs for the same guid,
/// one call later with nothing in between that could invalidate it.
/// Whenever <c>RebucketEquippedChildPresentation</c> would see no current
/// projection, <c>TryResolveExactAttachment</c> already failed first and
/// <c>TickChild</c> already returned <see langword="false"/> at
/// <c>:442</c>, never reaching the rebucket call at all. The
/// <c>NoProjection</c> check inside <c>RebucketEquippedChildPresentation</c>
/// is therefore defensive/unreachable from this call site as currently
/// structured, not a live withdrawal trigger — left in place because it
/// is the correct contract for the method taken on its own (any other
/// caller, or a future refactor that removes/reorders the
/// <c>TryResolveExactAttachment</c> gate, could reach it), but it is not
/// exercisable end-to-end through <c>TickChild</c> today.
/// </para>
///
/// <para>
/// This test therefore calls <c>RebucketEquippedChildPresentation</c>
/// directly — the same technique already used by
/// <c>ControllerFixture.RemoveProjectionOnly</c> to reach the abnormal
/// state — proving the disposition VALUE is correct in isolation, rather
/// than asserting a TickChild-level trigger that does not exist. The
/// earlier "pose-loss recovery also no-ops here" finding (reported, not
/// fixed, in the prior revision of this comment) is superseded by this
/// finding: since TickChild never reaches <c>NoProjection</c> from this
/// state, <c>WithdrawForPoseLoss</c>'s interaction with it is moot.
/// </para>
/// </summary>
[Fact]
public void RebucketEquippedChildPresentation_D4_NoLiveProjection_ReturnsNoProjectionDisposition()
{
using var fixture = new ControllerFixture((_, _, _) =>
new(ExactProjectionWithdrawalDisposition.Completed, Failure: null));
LiveEntityRecord parent = fixture.Spawn(0x70000264u, generation: 1);
LiveEntityRecord child = fixture.Spawn(
0x70000265u,
generation: 1,
LiveEntityProjectionKind.Attached);
fixture.InstallAttached(parent, child);
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
var relation = new ParentAttachmentRelation(
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1);
fixture.CommitRenderedRelation(relation);
// The relation stays committed (unlike the NotAttached test) — only
// the projection itself vanishes, which is the ABNORMAL case
// NoProjection exists to catch.
Assert.True(fixture.Live.ParentAttachments.HasCommittedParent(
child.ServerGuid));
fixture.RemoveProjectionOnly(child);
EquippedChildPresentationRebucketDisposition disposition =
fixture.Live.RebucketEquippedChildPresentation(
child.ServerGuid,
0x01010001u);
Assert.Equal(
EquippedChildPresentationRebucketDisposition.NoProjection,
disposition);
}
[Fact]
public void AcceptedValidParent_WithdrawsWorldProjectionBeforePosePrerequisitesExist()
{
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
fixture.Spawn(0x70000250u, generation: 1);
LiveEntityRecord child = fixture.Spawn(0x70000251u, generation: 1);
fixture.Controller.OnParentEvent(new ParentEvent.Parsed(
ParentGuid: 0x70000250u,
ChildGuid: 0x70000251u,
ParentLocation: 0,
PlacementId: 0,
ParentInstanceSequence: 1,
ChildPositionSequence: 2));
Assert.False(child.IsSpatiallyProjected);
Assert.True(fixture.Live.ParentAttachments.TryGetProjection(
0x70000251u,
out _));
}
}
[Fact]
public void InvalidParent_ConsumesPositionTimestampButPreservesWorldProjection()
{
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
fixture.Spawn(0x70000252u, generation: 1);
LiveEntityRecord child = fixture.Spawn(0x70000253u, generation: 1);
fixture.Controller.OnParentEvent(new ParentEvent.Parsed(
ParentGuid: 0x70000252u,
ChildGuid: 0x70000253u,
ParentLocation: 1,
PlacementId: 0,
ParentInstanceSequence: 1,
ChildPositionSequence: 2));
Assert.True(child.IsSpatiallyProjected);
Assert.NotEqual(0u, child.FullCellId);
Assert.True(fixture.Live.TryGetSnapshot(
child.ServerGuid,
out WorldSession.EntitySpawn snapshot));
Assert.NotNull(snapshot.Position);
Assert.Null(snapshot.ParentGuid);
Assert.Equal((ushort)2, snapshot.PositionSequence);
Assert.False(fixture.Live.ParentAttachments.TryGetProjection(
child.ServerGuid,
out _));
}
}
[Fact]
public void QueuedValidThenInvalidParent_LeavesValidParentCommitted()
{
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
LiveEntityRecord validParent = fixture.Spawn(0x70000259u, generation: 1);
LiveEntityRecord invalidParent = fixture.Spawn(0x7000025Au, generation: 1);
const uint childGuid = 0x7000025Bu;
fixture.Controller.OnParentEvent(new ParentEvent.Parsed(
validParent.ServerGuid, childGuid, 0, 0, 1, 1));
fixture.Controller.OnParentEvent(new ParentEvent.Parsed(
invalidParent.ServerGuid, childGuid, 1, 0, 1, 2));
RuntimeEntityRecord childIdentity = fixture.RegisterOnly(
childGuid,
generation: 1,
hasPosition: true);
LiveEntityRecord child = fixture.Materialize(childIdentity);
Assert.True(fixture.Live.TryGetSnapshot(
childGuid,
out WorldSession.EntitySpawn childSpawn));
fixture.Controller.OnSpawn(childSpawn);
Assert.True(fixture.Live.TryGetSnapshot(childGuid, out childSpawn));
Assert.Equal(validParent.ServerGuid, childSpawn.ParentGuid);
Assert.Null(childSpawn.Position);
Assert.Equal((ushort)2, childSpawn.PositionSequence);
var valid = new ParentAttachmentRelation(
validParent.ServerGuid, childGuid, 0, 0, 1, 1);
Assert.True(fixture.Live.ParentAttachments.IsCommitted(valid));
Assert.False(fixture.Live.ParentAttachments.TryGetStagedProjection(
childGuid,
out _));
}
}
[Fact]
public void NoPositionCreateParent_CommitsAfterParentPartArrayValidation()
{
using var fixture = new ControllerFixture((_, _, _) =>
new ExactProjectionWithdrawalOutcome(
ExactProjectionWithdrawalDisposition.Completed,
Failure: null));
LiveEntityRecord parent = fixture.Spawn(0x70000254u, generation: 1);
RuntimeEntityRecord child = fixture.RegisterOnly(
0x70000255u,
generation: 1,
hasPosition: false);
var update = new CreateParentUpdate(
child.ServerGuid,
parent.ServerGuid,
ParentLocation: 0,
PlacementId: 0,
ChildInstanceSequence: 1,
ChildPositionSequence: 1);
fixture.CompleteFirstEntry();
Assert.True(fixture.Live.TryApplyCreateParent(update, out _));
fixture.Controller.OnCreateParentAccepted(update);
Assert.True(fixture.Live.TryGetSnapshot(
child.ServerGuid,
out WorldSession.EntitySpawn snapshot));
Assert.Equal(parent.ServerGuid, snapshot.ParentGuid);
Assert.Null(snapshot.Position);
Assert.False(fixture.Live.TryGetRecord(child.ServerGuid, out _));
// #319: the parent was spawned with generation 1 - the committed
// relation must carry the PARENT's live incarnation (late-bound at
// accept time), not a hardcoded 0. This assertion previously pinned
// the exact #319 defect (it passed for a nonzero-incarnation parent
// only because AcceptCreateObjectRelation ignored the parent
// entirely and always staged 0).
var relation = new ParentAttachmentRelation(
parent.ServerGuid,
child.ServerGuid,
0,
0,
1,
1);
Assert.True(fixture.Live.ParentAttachments.IsCommitted(relation));
fixture.Poses.Publish(
parent.WorldEntity!,
Array.Empty<Matrix4x4>());
fixture.Controller.OnPosePublished(parent.ServerGuid);
Assert.True(fixture.Live.TryGetRecord(
child.ServerGuid,
out LiveEntityRecord childProjection));
Assert.NotNull(childProjection.WorldEntity);
Assert.True(childProjection.IsSpatiallyProjected);
Assert.Equal(
LiveEntityProjectionKind.Attached,
childProjection.ProjectionKind);
}
[Fact]
public void SpawnParentWithoutAnimationFrame_UsesRetailPlacementZero()
{
using var fixture = new ControllerFixture((_, _, _) =>
new ExactProjectionWithdrawalOutcome(
ExactProjectionWithdrawalDisposition.Completed,
Failure: null));
LiveEntityRecord parent = fixture.Spawn(0x70000270u, generation: 1);
WorldSession.EntitySpawn childSpawn = ControllerFixture.SpawnData(
0x70000271u,
generation: 1);
childSpawn = childSpawn with
{
Position = null,
ParentGuid = parent.ServerGuid,
ParentLocation = 0,
PlacementId = null,
PositionSequence = 0,
Physics = childSpawn.Physics!.Value with
{
Position = null,
Parent = new PhysicsAttachment(parent.ServerGuid, 0u),
AnimationFrame = null,
},
};
fixture.Live.RegisterLiveEntity(childSpawn);
fixture.Controller.OnSpawn(childSpawn);
// #319: the parent was spawned with generation 1 - see the sibling
// fix note in NoPositionCreateParent_CommitsAfterParentPartArrayValidation
// above.
var expected = new ParentAttachmentRelation(
parent.ServerGuid,
childSpawn.Guid,
ParentLocation: 0,
PlacementId: 0,
ParentInstanceSequence: 1,
ChildPositionSequence: 0);
Assert.True(fixture.Live.ParentAttachments.IsCommitted(expected));
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
fixture.Controller.OnPosePublished(parent.ServerGuid);
Assert.True(fixture.Live.TryGetRecord(
childSpawn.Guid,
out LiveEntityRecord child));
Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind);
Assert.NotNull(child.WorldEntity);
}
/// <summary>
/// #319 F1, the key itself. Dual-parent-class matrix: a player-range
/// parent (nonzero incarnation) and a creature-range parent (sequence
/// 0). Drives the REAL producer (<see cref="EquippedChildRenderController.OnSpawn"/>)
/// rather than the test helper <c>CommitRenderedRelation</c> most other
/// tests in this file use (which hand-crafts the relation and never
/// exercises the hardcoded-0 bug). Verifies the relation commits under
/// the PARENT's exact live incarnation (not 0) and that both D1 (attach
/// re-cell) and D2 (crossing propagation) reach the child.
/// </summary>
[Theory]
[InlineData(0x50000777u, (ushort)9)]
[InlineData(0x70000280u, (ushort)0)]
public void OnSpawn_CreateObjectRelation_LateBindsToParentLiveIncarnationAndPropagatesCell(
uint parentGuid,
ushort parentIncarnation)
{
using var fixture = new ControllerFixture((_, _, _) =>
new ExactProjectionWithdrawalOutcome(
ExactProjectionWithdrawalDisposition.Completed,
Failure: null));
LiveEntityRecord parent = fixture.Spawn(parentGuid, generation: parentIncarnation);
Assert.NotEqual(0u, parent.Canonical.FullCellId);
WorldSession.EntitySpawn childSpawn = ControllerFixture.SpawnData(
0x7000028Fu,
generation: 1);
childSpawn = childSpawn with
{
Position = null,
ParentGuid = parent.ServerGuid,
ParentLocation = 0,
PlacementId = null,
PositionSequence = 0,
Physics = childSpawn.Physics!.Value with
{
Position = null,
Parent = new PhysicsAttachment(parent.ServerGuid, 0u),
AnimationFrame = null,
},
};
fixture.Live.RegisterLiveEntity(childSpawn);
// SABOTAGE PROBE: restoring the literal `ParentInstanceSequence: 0`
// at EquippedChildRenderController.OnSpawn makes the player row
// (parentIncarnation=9) fail this exact assertion while the
// creature row (parentIncarnation=0) keeps passing - the precise
// #319 signature.
fixture.Controller.OnSpawn(childSpawn);
Assert.True(fixture.Live.ParentAttachments.TryGetCommittedParent(
childSpawn.Guid,
out uint committedParentGuid,
out ushort committedInstance));
Assert.Equal(parent.ServerGuid, committedParentGuid);
Assert.Equal(parentIncarnation, committedInstance);
// D1: attach re-cells the child to the parent's exact cell, inside
// the same synchronous OnSpawn call (CommitAcceptedParentCellless
// runs regardless of whether the render projection itself is ready
// to materialize).
Assert.True(fixture.Live.TryGetCanonical(
childSpawn.Guid,
out RuntimeEntityRecord childCanonical));
Assert.Equal(parent.Canonical.FullCellId, childCanonical.FullCellId);
// D2: a later parent canonical-cell write propagates to the
// committed child.
const uint newCell = 0x01020001u;
const uint newLandblock = 0x0102FFFFu;
fixture.Spatial.AddLandblock(new LoadedLandblock(
newLandblock, new LandBlock(), Array.Empty<WorldEntity>()));
Assert.True(fixture.EntityObjects.CommitRebucket(
parent.Canonical, newCell, newLandblock));
Assert.Equal(newCell, childCanonical.FullCellId);
}
/// <summary>
/// #319 A1 (architecture review, 2026-08-05), the ordering fix's own
/// end-to-end proof. Dual-parent-class matrix. A relation whose named
/// incarnation mismatches the parent's live value must be refused
/// BEFORE the canonical commit runs, not after - the original shape
/// let <c>CommitStagedParent</c> (the canonical half: nulls the
/// child's snapshot Position, writes ParentGuid, advances POSITION_TS)
/// run first, then threw from inside <c>CommitProjection</c>, leaving
/// the child canonically parented with no committed relation. Stages
/// the mismatch directly (the real producer cannot construct one - it
/// always resolves the parent's TRUE live incarnation) to isolate the
/// ordering guarantee itself.
/// </summary>
[Theory]
[InlineData(0x50000821u, (ushort)6)]
[InlineData(0x70000299u, (ushort)0)]
public void PrepareAndTryRealize_MismatchedIncarnation_RefusesBeforeCanonicalCommit(
uint parentGuid,
ushort parentIncarnation)
{
using var fixture = new ControllerFixture((_, _, _) =>
new ExactProjectionWithdrawalOutcome(
ExactProjectionWithdrawalDisposition.Completed,
Failure: null));
LiveEntityRecord parent = fixture.Spawn(parentGuid, generation: parentIncarnation);
const uint childGuid = 0x700002A0u;
fixture.RegisterOnly(childGuid, generation: 1, hasPosition: true);
// #319 B1 (architecture review round 2, 2026-08-05): ChildPositionSequence
// MUST equal the fixture's actual gate value (RegisterOnly/SpawnData
// leaves Timestamps.Position and the top-level PositionSequence at 0,
// and unlike NoPositionCreateParent_CommitsAfterParentPartArrayValidation
// this test never calls TryApplyCreateParent/TryApplyParent to advance
// it). A mismatched value here would make TryCommitParent's OWN
// POSITION_TS gate (InboundPhysicsStateController.cs:299-312) refuse
// the canonical commit regardless of ordering, so the three snapshot
// assertions below would pass vacuously under a REVERTED ordering too
// - the test would "fail under sabotage" only on the unrelated staged-
// projection assertion, proving nothing about A1's tearing fix.
var wrongRelation = new ParentAttachmentRelation(
parent.ServerGuid,
childGuid,
ParentLocation: 0,
PlacementId: 0,
ParentInstanceSequence: (ushort)(parentIncarnation + 1),
ChildPositionSequence: 0);
fixture.Live.ParentAttachments.AcceptCreateObjectRelation(wrongRelation);
fixture.Controller.OnWorldEntityRegistered(parent.ServerGuid);
// The canonical layer must NEVER observe this child as parented -
// if the ordering regressed, snapshot.Position would be null and
// ParentGuid would be set even though no relation ever committed.
Assert.True(fixture.Live.TryGetSnapshot(
childGuid,
out WorldSession.EntitySpawn snapshot));
Assert.Null(snapshot.ParentGuid);
Assert.NotNull(snapshot.Position);
Assert.False(fixture.Live.ParentAttachments.TryGetCommittedParent(
childGuid,
out _,
out _));
// The refused relation does not linger and block Resolve forever -
// it is rejected, not stranded.
Assert.False(fixture.Live.ParentAttachments.TryGetStagedProjection(
childGuid,
out _));
}
/// <summary>
/// #319 A6 (architecture review, 2026-08-05). A prior revision of this
/// fix deferred a CreateObject-carried relation whose parent was not
/// yet addressable through a queue. Both reviews independently proved
/// that shape is structurally unreachable in production for BOTH
/// producers (raw CreateObject via <c>OnSpawn</c> AND the same-
/// generation <see cref="CreateParentUpdate"/> envelope via
/// <c>OnCreateParentAccepted</c>):
/// <c>RuntimeEntityObjectLifetime.RegisterEntityCore</c>'s
/// <c>EnqueueDeferredCreate</c> gate defers the ENTIRE CreateObject
/// whenever its parent is not yet active, and does so BEFORE either
/// wire shape is ever produced - the earlier version of this test
/// only reached the deferred branch by calling
/// <c>OnCreateParentAccepted</c> directly, bypassing that upstream
/// gate. The deferral queue carried three independent defects (a
/// missing child POSITION_TS gate, a placeholder-incarnation collision
/// with the generation filters, unbounded accumulation) while never
/// being exercised by production routing, so it was removed rather
/// than fixed in place. This test now pins the CORRECTED behavior: an
/// unaddressable parent at accept time is refused outright (logged,
/// no state mutation, no crash) - dual-parent-class matrix.
/// </summary>
[Theory]
[InlineData(0x50000811u, (ushort)4)]
[InlineData(0x70000291u, (ushort)0)]
public void OnCreateParentAccepted_ParentNotYetKnown_RefusesWithoutStateOrCrash(
uint parentGuid,
ushort parentIncarnation)
{
using var fixture = new ControllerFixture((_, _, _) =>
new ExactProjectionWithdrawalOutcome(
ExactProjectionWithdrawalDisposition.Completed,
Failure: null));
const uint childGuid = 0x7000029Fu;
fixture.RegisterOnly(childGuid, generation: 1, hasPosition: false);
fixture.CompleteFirstEntry();
var update = new CreateParentUpdate(
childGuid,
parentGuid,
ParentLocation: 0,
PlacementId: 0,
ChildInstanceSequence: 1,
ChildPositionSequence: 1);
// The parent has not spawned yet - the relation must not be staged,
// queued, or committed under any key, and the call must not throw.
Assert.True(fixture.Live.TryApplyCreateParent(update, out _));
fixture.Controller.OnCreateParentAccepted(update);
Assert.False(fixture.Live.ParentAttachments.TryGetStagedProjection(
childGuid,
out _));
Assert.False(fixture.Live.ParentAttachments.TryGetCommittedParent(
childGuid,
out _,
out _));
Assert.Equal(0, fixture.Live.ParentAttachments.UnresolvedRelationCount);
// The parent later becoming addressable does NOT retroactively
// attach the refused relation - there is nothing left to retry
// (the earlier deferred-queue design would have found and
// committed it here; the corrected design does not).
LiveEntityRecord parent = fixture.Spawn(parentGuid, generation: parentIncarnation);
fixture.Controller.OnWorldEntityRegistered(parent.ServerGuid);
Assert.False(fixture.Live.ParentAttachments.TryGetCommittedParent(
childGuid,
out _,
out _));
}
[Fact]
public void ObjDesc_ReprojectsAttachedChildWithoutWorldPositionOrIdentityChange()
{
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
LiveEntityRecord parent = fixture.Spawn(0x70000272u, generation: 1);
WorldSession.EntitySpawn childSpawn = ControllerFixture.SpawnData(
0x70000273u,
generation: 1);
childSpawn = childSpawn with
{
Position = null,
ParentGuid = parent.ServerGuid,
ParentLocation = 0,
PlacementId = 0,
PositionSequence = 0,
Physics = childSpawn.Physics!.Value with
{
Position = null,
Parent = new PhysicsAttachment(parent.ServerGuid, 0u),
AnimationFrame = 0,
},
};
fixture.Live.RegisterLiveEntity(childSpawn);
fixture.Controller.OnSpawn(childSpawn);
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
fixture.Controller.OnPosePublished(parent.ServerGuid);
Assert.True(fixture.Live.TryGetRecord(
childSpawn.Guid,
out LiveEntityRecord child));
WorldEntity entity = child.WorldEntity!;
Assert.Null(entity.PaletteOverride);
WorldSession.EntitySpawn grandchildSpawn = ControllerFixture.SpawnData(
0x70000274u,
generation: 1);
grandchildSpawn = grandchildSpawn with
{
Position = null,
ParentGuid = child.ServerGuid,
ParentLocation = 0,
PlacementId = 0,
PositionSequence = 0,
Physics = grandchildSpawn.Physics!.Value with
{
Position = null,
Parent = new PhysicsAttachment(child.ServerGuid, 0u),
AnimationFrame = 0,
},
};
fixture.Live.RegisterLiveEntity(grandchildSpawn);
fixture.Controller.OnSpawn(grandchildSpawn);
Assert.True(fixture.Live.TryGetRecord(
grandchildSpawn.Guid,
out LiveEntityRecord grandchild));
WorldEntity grandchildEntity = grandchild.WorldEntity!;
Assert.Equal(
LiveEntityProjectionKind.Attached,
grandchild.ProjectionKind);
Assert.True(fixture.Live.TryApplyObjDesc(
new ObjDescEvent.Parsed(
child.ServerGuid,
new CreateObject.ModelData(
BasePaletteId: 0x04000022u,
[new CreateObject.SubPaletteSwap(
0x0F000033u,
Offset: 4,
Length: 8)],
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.AnimPartChange>()),
InstanceSequence: 1,
ObjDescSequence: 1),
out _));
Assert.Null(child.Snapshot.Position);
Assert.True(fixture.Controller.TryApplyAttachedAppearance(
child,
child.ObjDescAuthorityVersion));
Assert.Same(entity, child.WorldEntity);
Assert.True(child.IsSpatiallyProjected);
Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind);
Assert.Equal((uint)0x04000022u, child.Snapshot.BasePaletteId);
Assert.NotNull(entity.PaletteOverride);
Assert.Equal(0x04000022u, entity.PaletteOverride!.BasePaletteId);
PaletteOverride.SubPaletteRange range =
Assert.Single(entity.PaletteOverride.SubPalettes);
Assert.Equal(0x0F000033u, range.SubPaletteId);
Assert.Equal((byte)4, range.Offset);
Assert.Equal((byte)8, range.Length);
Assert.Same(grandchildEntity, grandchild.WorldEntity);
Assert.True(grandchild.IsSpatiallyProjected);
Assert.Equal(
LiveEntityProjectionKind.Attached,
grandchild.ProjectionKind);
}
}
[Fact]
public void ChildWithoutSetup_StillCommitsLogicalParenting()
{
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
LiveEntityRecord parent = fixture.Spawn(0x7000025Cu, generation: 1);
RuntimeEntityRecord childIdentity = fixture.RegisterOnly(
0x7000025Du,
generation: 1,
hasPosition: true,
hasSetup: false);
LiveEntityRecord child = fixture.Materialize(childIdentity);
child.HasPartArray = false;
var update = new ParentEvent.Parsed(
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1);
fixture.Controller.OnParentEvent(update);
Assert.True(fixture.Live.TryGetSnapshot(
child.ServerGuid,
out WorldSession.EntitySpawn snapshot));
Assert.Equal(parent.ServerGuid, snapshot.ParentGuid);
Assert.Null(snapshot.Position);
Assert.False(child.IsSpatiallyProjected);
Assert.True(fixture.Live.ParentAttachments.IsCommitted(new(
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1)));
}
}
[Fact]
public void EmbeddedSameGenerationCreateParent_UsesStagedRoute()
{
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
LiveEntityRecord parent = fixture.Spawn(0x7000025Eu, generation: 1);
LiveEntityRecord child = fixture.Spawn(0x7000025Fu, generation: 1);
PhysicsTimestamps timestamps = new(
Position: 1,
Movement: 0,
State: 0,
Vector: 0,
Teleport: 0,
ServerControlledMove: 0,
ForcePosition: 0,
ObjDesc: 0,
Instance: 1);
PhysicsSpawnData physics = new(
RawState: 0x408u,
Position: null,
Movement: null,
AnimationFrame: 0,
SetupTableId: 0x02000001u,
MotionTableId: null,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: new PhysicsAttachment(parent.ServerGuid, 0),
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
WorldSession.EntitySpawn incoming = ControllerFixture.SpawnData(
child.ServerGuid,
generation: 1) with
{
Position = null,
PositionSequence = 1,
PlacementId = 0,
ParentGuid = parent.ServerGuid,
ParentLocation = 0,
Physics = physics,
};
LiveEntityRegistrationResult refresh = fixture.Live.RegisterLiveEntity(incoming);
CreateParentUpdate update = Assert.IsType<CreateParentUpdate>(
refresh.Inbound.SameGenerationEvents!.Value.Parent);
Assert.True(fixture.Live.TryApplyCreateParent(update, out _));
fixture.Controller.OnCreateParentAccepted(update);
Assert.True(fixture.Live.TryGetSnapshot(
child.ServerGuid,
out WorldSession.EntitySpawn snapshot));
Assert.Equal(parent.ServerGuid, snapshot.ParentGuid);
Assert.Null(snapshot.Position);
}
}
[Fact]
public void NewWaitingParent_DoesNotDisplaceCommittedRecovery()
{
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
LiveEntityRecord oldParent = fixture.Spawn(0x70000256u, generation: 1);
LiveEntityRecord child = fixture.Spawn(
0x70000257u,
generation: 1,
LiveEntityProjectionKind.Attached);
RuntimeEntityRecord waitingParent = fixture.RegisterOnly(
0x70000258u,
generation: 1,
hasPosition: true);
var oldRelation = new ParentAttachmentRelation(
oldParent.ServerGuid,
child.ServerGuid,
0,
0,
1,
1);
fixture.Live.ParentAttachments.AcceptCreateObjectRelation(oldRelation);
Assert.True(fixture.Live.TryApplyParent(new ParentEvent.Parsed(
oldRelation.ParentGuid,
oldRelation.ChildGuid,
oldRelation.ParentLocation,
oldRelation.PlacementId,
oldRelation.ParentInstanceSequence,
oldRelation.ChildPositionSequence), out _));
Assert.True(fixture.Live.CommitStagedParent(oldRelation, out _));
Assert.True(fixture.Live.ParentAttachments.CommitProjection(oldRelation));
fixture.Live.ParentAttachments.MarkProjected(
oldRelation,
ParentProjectionCandidateKind.Recovery);
fixture.InstallAttached(oldParent, child);
var newer = new ParentEvent.Parsed(
waitingParent.ServerGuid,
child.ServerGuid,
ParentLocation: 0,
PlacementId: 0,
ParentInstanceSequence: 1,
ChildPositionSequence: 2);
fixture.Controller.OnParentEvent(newer);
Assert.True(fixture.Live.ParentAttachments.TryGetStagedProjection(
child.ServerGuid,
out ParentAttachmentRelation staged));
Assert.Equal(waitingParent.ServerGuid, staged.ParentGuid);
fixture.Controller.Tick();
Assert.Empty(fixture.Controller.AttachedEntityIds);
Assert.True(fixture.Live.ParentAttachments.TryGetStagedProjection(
child.ServerGuid,
out staged));
Assert.Equal(waitingParent.ServerGuid, staged.ParentGuid);
Assert.True(fixture.Live.ParentAttachments.TryGetRecoveryProjection(
child.ServerGuid,
out ParentAttachmentRelation recovery));
Assert.Equal(oldParent.ServerGuid, recovery.ParentGuid);
fixture.Materialize(waitingParent);
fixture.Controller.OnWorldEntityRegistered(waitingParent.ServerGuid);
Assert.True(fixture.Live.ParentAttachments.IsCommitted(staged));
Assert.True(fixture.Live.ParentAttachments.TryGetRecoveryProjection(
child.ServerGuid,
out ParentAttachmentRelation committedRecovery));
Assert.Equal(staged, committedRecovery);
Assert.True(fixture.Live.TryGetSnapshot(
child.ServerGuid,
out WorldSession.EntitySpawn snapshot));
Assert.Equal(waitingParent.ServerGuid, snapshot.ParentGuid);
}
}
[Fact]
public void OrdinaryRemoval_PendingFailureRetriesExactCaptureOnTick()
{
int attempts = 0;
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
attempts++;
if (attempts == 1)
{
return new(
ExactProjectionWithdrawalDisposition.Pending,
new InvalidOperationException("ordinary cleanup failed"));
}
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
LiveEntityRecord parent = fixture.Spawn(0x70000260u, generation: 1);
LiveEntityRecord child = fixture.Spawn(
0x70000261u,
generation: 1,
LiveEntityProjectionKind.Attached);
fixture.InstallAttached(parent, child);
fixture.Live.ParentAttachments.AcceptCreateObjectRelation(new(
parent.ServerGuid,
child.ServerGuid,
ParentLocation: 0,
PlacementId: 0,
ParentInstanceSequence: 1,
ChildPositionSequence: 1));
TargetInvocationException error = Assert.Throws<TargetInvocationException>(() =>
fixture.InvokeOrdinaryRemoval(0x70000261u));
Assert.IsType<InvalidOperationException>(error.InnerException);
fixture.Controller.Tick();
Assert.Equal(2, attempts);
Assert.Empty(fixture.Controller.AttachedEntityIds);
Assert.False(child.IsSpatiallyProjected);
Assert.False(fixture.Live.ParentAttachments.TryGetProjection(
child.ServerGuid,
out _));
}
}
[Fact]
public void DetachedRemoval_PendingFailureRetriesAndPreservesRollbackHistory()
{
int attempts = 0;
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
attempts++;
if (attempts == 1)
{
return new(
ExactProjectionWithdrawalDisposition.Pending,
new InvalidOperationException("detached cleanup failed"));
}
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
LiveEntityRecord parent = fixture.Spawn(0x70000262u, generation: 1);
LiveEntityRecord child = fixture.Spawn(
0x70000263u,
generation: 1,
LiveEntityProjectionKind.Attached);
fixture.InstallAttached(parent, child);
var relation = new ParentAttachmentRelation(
parent.ServerGuid,
child.ServerGuid,
ParentLocation: 0,
PlacementId: 0,
ParentInstanceSequence: 1,
ChildPositionSequence: 1);
fixture.Live.ParentAttachments.AcceptCreateObjectRelation(relation);
Assert.True(fixture.Live.ParentAttachments.CommitProjection(relation));
fixture.Live.ParentAttachments.MarkProjected(
relation,
ParentProjectionCandidateKind.Recovery);
TargetInvocationException error = Assert.Throws<TargetInvocationException>(() =>
fixture.InvokeDetachedRemoval(child.ServerGuid));
Assert.IsType<InvalidOperationException>(error.InnerException);
fixture.Controller.Tick();
Assert.Equal(2, attempts);
Assert.Empty(fixture.Controller.AttachedEntityIds);
Assert.False(child.IsSpatiallyProjected);
Assert.True(fixture.Live.ParentAttachments.RestoreLastAccepted(
child.ServerGuid));
Assert.True(fixture.Live.ParentAttachments.TryGetRecoveryProjection(
child.ServerGuid,
out ParentAttachmentRelation restored));
Assert.Equal(relation, restored);
}
}
[Fact]
public void Unparent_WithdrawsCompleteAttachedSubtreeAndRecoversDescendants()
{
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
LiveEntityRecord parent = fixture.Spawn(0x70000264u, generation: 1);
LiveEntityRecord child = fixture.Spawn(
0x70000265u,
generation: 1,
LiveEntityProjectionKind.Attached);
LiveEntityRecord grandchild = fixture.Spawn(
0x70000266u,
generation: 1,
LiveEntityProjectionKind.Attached);
fixture.InstallAttached(parent, child);
fixture.InstallAttached(child, grandchild);
var childRelation = new ParentAttachmentRelation(
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1);
var grandchildRelation = new ParentAttachmentRelation(
child.ServerGuid, grandchild.ServerGuid, 0, 0, 1, 1);
fixture.CommitRenderedRelation(childRelation);
fixture.CommitRenderedRelation(grandchildRelation);
ChildUnparentDisposition result =
fixture.Controller.OnChildBecameUnparented(child.ServerGuid);
Assert.Equal(ChildUnparentDisposition.Completed, result);
Assert.Empty(fixture.Controller.AttachedEntityIds);
Assert.False(child.IsSpatiallyProjected);
Assert.False(grandchild.IsSpatiallyProjected);
Assert.False(fixture.Live.ParentAttachments.RestoreLastAccepted(
child.ServerGuid));
Assert.True(fixture.Live.ParentAttachments.TryGetRecoveryProjection(
grandchild.ServerGuid,
out ParentAttachmentRelation recovered));
Assert.Equal(grandchildRelation, recovered);
}
}
[Fact]
public void PendingDetachedRemoval_RollbackCancelsRetryAndKeepsProjection()
{
int attempts = 0;
using var fixture = new ControllerFixture((_, _, _) =>
{
attempts++;
return new(
ExactProjectionWithdrawalDisposition.Pending,
new InvalidOperationException("component withdrawal failed"));
});
LiveEntityRecord parent = fixture.Spawn(0x70000267u, generation: 1);
LiveEntityRecord child = fixture.Spawn(
0x70000268u,
generation: 1,
LiveEntityProjectionKind.Attached);
fixture.InstallAttached(parent, child);
fixture.CommitRenderedRelation(new(
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1));
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
fixture.Objects.AddOrUpdate(new ClientObject
{
ObjectId = child.ServerGuid,
WielderId = parent.ServerGuid,
CurrentlyEquippedLocation = EquipMask.MeleeWeapon,
});
Assert.Throws<InvalidOperationException>(() =>
fixture.Objects.MoveItemOptimistic(
child.ServerGuid,
newContainerId: 0x50000001u,
newSlot: 0));
Assert.True(fixture.Objects.RollbackMove(child.ServerGuid));
fixture.Controller.Tick();
Assert.Equal(1, attempts);
Assert.True(child.IsSpatiallyProjected);
Assert.Single(fixture.Controller.AttachedEntityIds);
}
[Fact]
public void InventoryPutObjectIn3D_DoesNotWithdrawRecoveredWorldProjection()
{
int withdrawals = 0;
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
withdrawals++;
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
const uint guid = 0x7000026Au;
const uint player = 0x50000001u;
LiveEntityRecord dropped = fixture.Spawn(guid, generation: 1);
fixture.Objects.AddOrUpdate(new ClientObject
{
ObjectId = guid,
ContainerId = player,
CurrentlyEquippedLocation = EquipMask.None,
});
Assert.True(fixture.Objects.ApplyConfirmedServerMove(
guid,
newContainerId: 0u,
newWielderId: 0u));
Assert.Equal(0, withdrawals);
Assert.True(dropped.IsSpatiallyProjected);
Assert.Equal(LiveEntityProjectionKind.World, dropped.ProjectionKind);
}
}
[Fact]
public void OrphanRollback_PendingFailureRetainsExactRetryOwner()
{
int attempts = 0;
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
attempts++;
if (attempts == 1)
{
return new(
ExactProjectionWithdrawalDisposition.Pending,
new InvalidOperationException("orphan component cleanup failed"));
}
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
LiveEntityRecord orphan = fixture.Spawn(0x70000269u, generation: 1);
TargetInvocationException error = Assert.Throws<TargetInvocationException>(() =>
fixture.InvokeOrphanRemoval(orphan));
Assert.IsType<InvalidOperationException>(error.InnerException);
fixture.Controller.Tick();
Assert.Equal(2, attempts);
Assert.False(orphan.IsSpatiallyProjected);
}
}
[Fact]
public void RecoveryContinuation_PostCommitFailureIsNotReplayed()
{
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
LiveEntityRecord record = fixture.Spawn(0x70000270u, generation: 1);
int continuations = 0;
Assert.Throws<InvalidOperationException>(() =>
fixture.Controller.OnChildBecameUnparented(
0x70000270u,
() =>
{
continuations++;
Assert.True(fixture.Live.RebucketLiveEntity(
0x70000270u,
0x01010001u));
throw new InvalidOperationException("observer failed after recovery");
}));
fixture.Controller.Tick();
Assert.Equal(1, continuations);
Assert.True(record.IsSpatiallyProjected);
}
}
private static LiveEntityRecord ChildRecord() =>
LiveEntityTestFixture.CreateExactProjectionRecord(
new WorldSession.EntitySpawn(
0x70000100u,
new CreateObject.ServerPosition(
0x01010001u, 0f, 0f, 0f, 1f, 0f, 0f, 0f),
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: null,
Name: "attached fixture",
ItemType: null,
MotionState: null,
MotionTableId: null,
InstanceSequence: 1));
private sealed class ControllerFixture : IDisposable
{
private const uint Cell = 0x01010001u;
private readonly DeferredLiveEntityRuntimeComponentLifecycle _lifecycle = new();
private readonly Setup _setup;
private readonly AcDream.Runtime.Session.RuntimeFirstEntryDriveController _firstEntry;
private sealed class NullCollisionSource
: AcDream.Content.IPreparedCollisionSource
{
public AcDream.Content.PreparedAssetPresence ProbeCollision(
AcDream.Content.Pak.PakAssetType type,
uint sourceFileId) =>
AcDream.Content.PreparedAssetPresence.Available;
public AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatSetupCollision> ReadSetupCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatSetupCollision>.Missing;
public AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatGfxObjCollisionAsset>
ReadGfxObjCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatCellStructureCollisionAsset>
ReadCellStructureCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public AcDream.Content.PreparedCollisionReadResult<
AcDream.Core.Physics.FlatEnvCellTopology> ReadEnvCellTopology(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public AcDream.Content.PreparedCollisionSourceStats CollisionStats =>
default;
public void Dispose()
{
}
}
internal ControllerFixture(
Func<LiveEntityRecord, ulong, ulong, ExactProjectionWithdrawalOutcome>
withdraw)
{
Spatial.AddLandblock(new LoadedLandblock(
(Cell & 0xFFFF0000u) | 0xFFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
EntityObjects = new RuntimeEntityObjectLifetime();
EntityObjects.BindEventContext(
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
static () => 1UL);
_firstEntry = new AcDream.Runtime.Session.RuntimeFirstEntryDriveController(
EntityObjects,
new AcDream.Runtime.GameRuntimeClock(),
new NullCollisionSource(),
() => AcDream.Runtime.Gameplay.PlayerMovementConstructionOptions.Fallback,
static _ => new AcDream.Runtime.Gameplay.RuntimeLocalPlayerPhysicsActivationPreparation(
0.48f,
1.835f,
AcDream.Runtime.Gameplay.RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
Live = new LiveEntityRuntime(
Spatial,
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }),
_lifecycle,
EntityObjects);
_setup = new Setup
{
HoldingLocations =
{
[(ParentLocation)0] = new LocationType
{
PartId = -1,
Frame = new Frame { Orientation = Quaternion.Identity },
},
},
};
IDatReaderWriter dat = DispatchProxy.Create<IDatReaderWriter, NullDatProxy>();
((NullDatProxy)(object)dat).Setup = _setup;
Controller = new EquippedChildRenderController(
dat,
new object(),
Objects,
Live,
Poses,
update => Live.TryApplyParent(update, out _),
withdraw,
new ShadowObjectRegistry(),
new PhysicsDataCache());
_lifecycle.Bind(new DelegateLiveEntityRuntimeComponentLifecycle(
Controller.OnLogicalTeardown));
}
internal GpuWorldState Spatial { get; } = new();
internal RuntimeEntityObjectLifetime EntityObjects { get; }
/// <summary>
/// C3c: completes fresh residences through the first-entry
/// conductors (a celless route performs no SetPosition, so its drain
/// completes synchronously), releasing the lease so legacy direct
/// CreateParent/Parent application stays valid post-flip. The
/// controller is constructed lazily but binds no notification of its
/// own tracking value until first use, so it sweeps active leases
/// directly instead.
/// </summary>
internal void CompleteFirstEntry()
{
_firstEntry.DriveAll();
}
internal ClientObjectTable Objects { get; } = new();
internal EntityEffectPoseRegistry Poses { get; } = new();
internal LiveEntityRuntime Live { get; }
internal EquippedChildRenderController Controller { get; }
internal LiveEntityRecord Spawn(
uint guid,
ushort generation,
LiveEntityProjectionKind kind = LiveEntityProjectionKind.World)
{
RuntimeEntityRecord canonical =
RegisterOnly(guid, generation, hasPosition: true);
return Materialize(canonical, kind);
}
internal RuntimeEntityRecord RegisterOnly(
uint guid,
ushort generation,
bool hasPosition,
bool hasSetup = true)
{
WorldSession.EntitySpawn spawn = SpawnData(guid, generation);
if (!hasPosition)
{
spawn = spawn with
{
Position = null,
Physics = spawn.Physics!.Value with { Position = null },
};
}
if (!hasSetup)
spawn = spawn with { SetupTableId = null };
return Assert.IsType<RuntimeEntityRecord>(
Live.RegisterLiveEntity(spawn).Canonical);
}
internal LiveEntityRecord Materialize(
RuntimeEntityRecord canonical,
LiveEntityProjectionKind kind = LiveEntityProjectionKind.World)
{
WorldEntity? entity = Live.MaterializeLiveEntity(
canonical,
Cell,
id => new WorldEntity
{
Id = id,
ServerGuid = canonical.ServerGuid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = Cell,
},
kind,
initializeProjection: null,
out LiveEntityRecord? projected);
Assert.NotNull(entity);
LiveEntityRecord record =
Assert.IsType<LiveEntityRecord>(projected);
record.HasPartArray = true;
return record;
}
internal static WorldSession.EntitySpawn SpawnData(uint guid, ushort generation)
{
// C3c: residence admission freezes the raw create and requires
// the flattened parser projections to agree with the nested
// PhysicsDesc block (HasConsistentCreateIdentityAndParent).
var position = new CreateObject.ServerPosition(
Cell, 0f, 0f, 0f, 1f, 0f, 0f, 0f);
var physics = new PhysicsSpawnData(
RawState: 0u,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: null,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: new PhysicsTimestamps(
Position: 0,
Movement: 0,
State: 0,
Vector: 0,
Teleport: 0,
ServerControlledMove: 0,
ForcePosition: 0,
ObjDesc: 0,
Instance: generation));
return new WorldSession.EntitySpawn(
guid,
position,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: null,
Name: "attached fixture",
ItemType: null,
MotionState: null,
MotionTableId: null,
InstanceSequence: generation,
Physics: physics);
}
internal void InstallAttached(
LiveEntityRecord parent,
LiveEntityRecord child)
{
Type attachedType = typeof(EquippedChildRenderController).GetNestedType(
"AttachedChild",
BindingFlags.NonPublic)!;
object attached = Activator.CreateInstance(
attachedType,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
binder: null,
args:
[
parent,
child,
parent.ServerGuid,
child.ServerGuid,
(ParentLocation)0,
(Placement)0,
_setup,
_setup,
Array.Empty<MeshRef>(),
Array.Empty<bool>(),
Array.Empty<Matrix4x4>(),
Array.Empty<MeshRef>(),
1f,
child.WorldEntity!,
],
culture: null)!;
FieldInfo mapField = typeof(EquippedChildRenderController).GetField(
"_attachedByChild",
BindingFlags.Instance | BindingFlags.NonPublic)!;
var map = (IDictionary)mapField.GetValue(Controller)!;
map.Add(child.ProjectionKey!.Value, attached);
}
/// <summary>
/// C4 route 7 D4 test support (N6, round-3 review): removes ONLY
/// <paramref name="record"/>'s <c>LiveEntityRuntime</c> projection
/// entry (via the same private <c>_projections</c> store
/// <c>MaterializeLiveEntity</c>'s own rollback paths use), leaving
/// any separately-installed <c>_attachedByChild</c> entry (from
/// <see cref="InstallAttached"/>) untouched. Constructs the
/// deliberately abnormal "stale AttachedChild, no live projection"
/// state <see cref="EquippedChildPresentationRebucketDisposition.NoProjection"/>
/// exists to catch — no production path is expected to reach it, so
/// this reflection is the test's own construction, not a stand-in
/// for a real caller.
/// </summary>
internal void RemoveProjectionOnly(LiveEntityRecord record)
{
FieldInfo projectionsField = typeof(LiveEntityRuntime).GetField(
"_projections",
BindingFlags.Instance | BindingFlags.NonPublic)!;
object projections = projectionsField.GetValue(Live)!;
MethodInfo removeActive = projections.GetType().GetMethod(
"RemoveActive",
BindingFlags.Instance | BindingFlags.Public)!;
Assert.True((bool)removeActive.Invoke(projections, [record])!);
}
internal void CommitRenderedRelation(ParentAttachmentRelation relation)
{
Live.ParentAttachments.AcceptCreateObjectRelation(relation);
Assert.True(Live.ParentAttachments.CommitProjection(relation));
Live.ParentAttachments.MarkProjected(
relation,
ParentProjectionCandidateKind.Recovery);
}
internal void InvokeOrdinaryRemoval(uint guid)
{
MethodInfo method = typeof(EquippedChildRenderController).GetMethod(
"TearDownCurrentObjectProjections",
BindingFlags.Instance | BindingFlags.NonPublic)!;
method.Invoke(Controller, [guid]);
}
internal void InvokeDetachedRemoval(uint guid)
{
MethodInfo method = typeof(EquippedChildRenderController).GetMethod(
"BeginDetachedRemoval",
BindingFlags.Instance | BindingFlags.NonPublic)!;
method.Invoke(Controller, [guid]);
}
internal void InvokeOrphanRemoval(LiveEntityRecord record)
{
FieldInfo mapField = typeof(EquippedChildRenderController).GetField(
"_pendingOrphanRemovalByChild",
BindingFlags.Instance | BindingFlags.NonPublic)!;
object map = mapField.GetValue(Controller)!;
MethodInfo method = typeof(EquippedChildRenderController).GetMethod(
"BeginProjectionSubtreeWithdrawal",
BindingFlags.Instance | BindingFlags.NonPublic)!;
method.Invoke(Controller, [map, record, false, false]);
}
public void Dispose() => Controller.Dispose();
}
private class NullDatProxy : DispatchProxy
{
internal Setup? Setup { get; set; }
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
{
if (targetMethod?.Name == "Get"
&& targetMethod.ReturnType == typeof(Setup))
{
return Setup;
}
if (targetMethod?.ReturnType == typeof(void))
return null;
if (targetMethod?.ReturnType.IsValueType == true)
return Activator.CreateInstance(targetMethod.ReturnType);
return null;
}
}
}