acdream/tests/AcDream.App.Tests/World/LiveEntityPresentationControllerTests.cs
Erik 6dc7ba51ee feat(physics): C4 route 4b-3 — remote teleport + cell-less through the canonical placement
Flips the last remote classification (SetPosition: teleport-advanced and
cell-less) onto 4b-1's RuntimeRemotePlacementDriveController, runs retail's
teleport_hook before the placement, and deletes the legacy remote-teleport
machinery. Contract: docs/research/2026-08-04-c4-route-4b-3-contract.md.

Retail: MoveOrTeleport @0x00516330's branch @0x00516386 -> teleport_hook
@0x005163EF -> SetFlags(0x1012) @0x00516414 -> SetPosition @0x00516420 ->
return 1 @0x00516438. The hook @0x00514ED0 runs BEFORE the placement and
regardless of its outcome. Retail places this branch unconditionally, at any
distance and any contact state (arg4 is read only @0x0051638E, after the
branch) — which is what retires AP-137's cell-less enqueue-vs-place delta.

D1 — the classifier's cell-less input is now the PRE-merge committed cell.
Retail's predicate is `this_1->cell == 0`, the BODY's own cell at
MoveOrTeleport entry (this_1 is assigned from this @0x00516334). acdream fed
the POST-merge canonical.FullCellId, which RefreshSnapshot ->
RefreshDerivedState -> SetFullCell has already stamped with the accepted wire
cell; a zero wire cell fails validation into RejectedData first. The shipped
remote cell-less predicate was therefore dead code, not merely different from
remotePlacementRequired. Threaded via a builder overload; route 1's overload
is untouched. The graphical !IsSpatiallyVisible arm of
projectionRequiresTeleportHook is deleted — a presentation predicate with no
retail analogue that fired the teleport machinery on a routine hot path.

Deleted: RemoteTeleportController (605), RemoteTeleportPlacement (85),
RemoteShadowPlacementSynchronizer (49), their 1,709 lines of tests, the
remotePlacementRequired predicate, the TeleportHookRequired plumbing, the
legacy pre-operation ConstrainTo fallback, and the player arm's legacy
!IsGrounded fallback. Net -2,030 lines.

Structural fix (two independent Opus reviews, round 1 FAIL/FAIL): three of the
four MAJORs were one defect — OnPosition carried two parallel inline copies of
the routing tail (player-guid, NPC-guid) that had drifted. Extracted
RunRemoteArmTail (3 call sites) and ApplyWireAirborneLeftoverBookkeeping (2),
both branches now share one implementation.

  A1  ToConstraintArm mapped AirborneSnap -> AirborneNoOperation, so the NPC
      arm armed ConstrainTo ZERO times for an out-of-contact wire-grounded
      creature — a regression this slice introduced while closing a
      structurally identical hole. Now maps to NearInterpolate; switch made
      total with a throwing default proven unreachable.
  R1  D2's write-nothing shape existed on the player arm only; NPC packets
      fell through and wrote the body. Retail makes no player/NPC distinction.
  R2  report_collision_end(this,1) @0x00514F31 was bound to
      ShadowObjects.Suspend, a port of a DIFFERENT retail function
      (remove_shadows_from_cells) that teleport_hook never calls. Now routes
      to RuntimeCollisionReportingState.LeaveWorld, which wraps the private
      ForceEnd in an admission-blocking transaction so a DoCollisionEnd
      callback cannot recreate the contact table.
  R3/A2 A teleported NPC synthesized ServerVelocity from the teleport distance
      (~1,000+ m/s) and planned a run cycle from it. Both the install and
      RemoteServerControlledVelocityCycle.Apply now gate on !isTeleportRoute.

BISECT HAZARD — A1's fix is correct only BECAUSE R1 landed. AirborneSnap is
reachable wire-airborne on the NPC arm only while D2's shape is missing there.
Reverting R1 alone silently inverts A1 into the opposite divergence: arming
where retail returns 0. Revert both or neither.

Also in the velocity hunk: the NPC block's two !IsPlayerGuid(update.Guid)
guards were dropped when it was wrapped in `if (!isTeleportRoute)`. Safe — all
five exit paths of the enclosing IsPlayerGuid block return, so the predicate is
unconditionally false below it — but it was unremarked by both reviews.

Register: AP-137 REWRITTEN (not deleted) to the surviving acdream-only
divergences — null classification during the login window and Rejected*
through UnroutedCatchUp keep a row. AD-42's RemoteTeleportController citation
retired; AP-136/AP-138 writer lists corrected to the two surviving non-Position
rebucket writers; AP-138 gains the teleport arm as a second producer of the
visible-without-collision residual (retirement path remains #309). AP-135 is
untouched and its two airborne bookkeeping writes are preserved on both arms.
AP-131 does not retire; #276 does not close.

Proof obligation 1: ParkCollisionResidents' overlap throw stays unreachable —
the teleport arm adds packets to the same TryBeginExclusiveAuthoredPlacement
one-operation-per-key machinery the far arm uses, opens no new operation shape,
and every DeferredCell outcome cancels synchronously with
restoreCancelledPark: true. The guarded property remains
HasOldPrefixPlacementDebt's stall, not a throw (4b-1's B2 caveat stands).

Correction to an earlier claim: LiveEntityPresentationController's
_activePlacementOwners was NOT write-never at HEAD —
remotePlacementRequired -> BeginPlacement -> Begin -> BeginAuthoritativePlacement
was a live writer chain. It becomes write-never BECAUSE this slice deletes that
chain, which is why deleting the dead half is behaviour-preserving.

Probe: ACDREAM_PROBE_REMOTE_TELEPORT=1 emits one [remote-teleport] line per
routed arm (guid, cause, hook-ran, placement status). TEMPORARY, strip with the
probe family.

Carried, disclosed not fixed: no dedicated bidirectional collision-partner test
for R2 (the wiring, not LeaveWorld itself, is what lacks coverage); the
stress test's teleport step drives hand-written field assignments rather than
the canonical arm; the per-packet runTeleportHook closure allocation (network
path, not the resolve path Slice I's 0 B discipline governs — file before
route 5 adds a fourth call site). B2: IRuntimeCollisionReportObserver has zero
production implementations, so retail's bidirectional DoCollisionEnd half still
reaches no gameplay consumer — this fix closes the wrong-function binding, not
that nobody listens.

Complete Release suite MEASURED at 11,013 passed / 4 skipped / 0 failed
(baseline 11,027/4/0; net -14 = ~33 deleted test cases against ~19 added).
Neither known flake fired (#302 PortalProjectionTests GC-allocation, #308
NakEmissionTests wall-clock).

STILL OWED: the two-client connected gate, which MUST use an NPC/creature
teleport target. Both round-1 MAJORs lived on the NPC arm and the velocity
cycle early-returns for 0x50xxxxxx guids, so a player target structurally
cannot observe A1, A2, or R3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:00:10 +02:00

743 lines
29 KiB
C#

using System.Numerics;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Content.Vfx;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Options;
using Xunit.Sdk;
namespace AcDream.App.Tests.World;
public sealed class LiveEntityPresentationControllerTests
{
[Fact]
public void InitialVisibleObject_DoesNotPlayUnHide()
{
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
Assert.Empty(fixture.TypedPlays);
Assert.True(fixture.Entity.IsDrawVisible);
Assert.Equal(1, fixture.Shadows.TotalRegistered);
}
[Fact]
public void HiddenThenVisible_PlaysTypedEffectsMutatesChildrenAndRestoresShadow()
{
Fixture fixture = new(
PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions);
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
Assert.Equal(
[(fixture.Entity.Id, LiveEntityPresentationController.HiddenScriptType, 1f)],
fixture.TypedPlays);
Assert.Equal([(Fixture.Guid, true)], fixture.ChildNoDraw);
Assert.Equal([Fixture.Guid], fixture.InvalidTargets);
Assert.Equal([fixture.Entity.Id], fixture.PartArrayEnterWorld);
Assert.Equal([0], fixture.PartArrayShadowCounts);
Assert.Equal(
["effect:76", "children:True", "part-array", "target"],
fixture.PresentationOrder);
Assert.Equal(0, fixture.Shadows.TotalRegistered);
Assert.False(fixture.Entity.IsDrawVisible);
Assert.True(fixture.Runtime.TryApplyState(
new SetState.Parsed(Fixture.Guid, 0u, 1, 2),
out _,
out _));
Assert.True(fixture.Controller.OnStateAccepted(Fixture.Guid));
Assert.Equal(
[
(fixture.Entity.Id, LiveEntityPresentationController.HiddenScriptType, 1f),
(fixture.Entity.Id, LiveEntityPresentationController.UnHideScriptType, 1f),
],
fixture.TypedPlays);
Assert.Equal([(Fixture.Guid, true), (Fixture.Guid, false)], fixture.ChildNoDraw);
Assert.Equal(
[fixture.Entity.Id, fixture.Entity.Id],
fixture.PartArrayEnterWorld);
Assert.Equal([0, 0], fixture.PartArrayShadowCounts);
Assert.Equal(
[
"effect:76", "children:True", "part-array", "target",
"effect:75", "children:False", "part-array",
],
fixture.PresentationOrder);
Assert.Equal(1, fixture.Shadows.TotalRegistered);
Assert.True(fixture.Entity.IsDrawVisible);
Assert.Same(
fixture.Entity,
Assert.Single(fixture.Runtime.VisibleRecords).WorldEntity);
}
[Fact]
public void SpellRecall_HiddenAndUnHide_RetireMagicTimelineThroughController()
{
const uint humanSetup = 0x02000001u;
const uint humanMotionTable = 0x09000001u;
const uint magic = 0x80000049u;
const uint ready = 0x41000003u;
const uint magicPortal = 0x40000038u;
string datDir = @"C:\Turbine\Asheron's Call";
if (!File.Exists(Path.Combine(datDir, "client_portal.dat")))
throw SkipException.ForSkip("Installed retail DATs are required.");
using var dats = new DatCollection(datDir, DatAccessType.Read);
Setup setup = Assert.IsType<Setup>(dats.Get<Setup>(humanSetup));
MotionTable table = Assert.IsType<MotionTable>(dats.Get<MotionTable>(humanMotionTable));
var sequencer = new AnimationSequencer(
setup,
table,
new RetailAnimationLoader(dats));
var fixture = new Fixture(
PhysicsStateFlags.ReportCollisions,
_ => sequencer.Manager.HandleEnterWorld());
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
sequencer.SetCycle(magic, ready);
sequencer.PlayAction(magicPortal);
Assert.False(sequencer.CurrentNodeDiag.IsLooping);
Assert.True(fixture.Runtime.TryApplyState(
new SetState.Parsed(
Fixture.Guid,
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions),
1,
2),
out _,
out _));
Assert.True(fixture.Controller.OnStateAccepted(Fixture.Guid));
Assert.True(sequencer.CurrentNodeDiag.IsLooping);
Assert.Empty(sequencer.Manager.PendingAnimations);
// A motion accepted while the mesh is hidden is retired again at
// retail's matching UnHide PartArray boundary before cell visibility
// returns.
var unhideSequencer = new AnimationSequencer(
setup,
table,
new RetailAnimationLoader(dats));
var unhideFixture = new Fixture(
PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions,
_ => unhideSequencer.Manager.HandleEnterWorld());
Assert.True(unhideFixture.Controller.OnLiveEntityReady(Fixture.Guid));
unhideSequencer.SetCycle(magic, ready);
unhideSequencer.PlayAction(magicPortal);
Assert.False(unhideSequencer.CurrentNodeDiag.IsLooping);
Assert.True(unhideFixture.Runtime.TryApplyState(
new SetState.Parsed(Fixture.Guid, 0u, 1, 3),
out _,
out _));
Assert.True(unhideFixture.Controller.OnStateAccepted(Fixture.Guid));
Assert.True(unhideSequencer.CurrentNodeDiag.IsLooping);
Assert.Empty(unhideSequencer.Manager.PendingAnimations);
Assert.Equal([0], fixture.PartArrayShadowCounts);
Assert.Equal([0, 0], unhideFixture.PartArrayShadowCounts);
Assert.Equal(1, unhideFixture.Shadows.TotalRegistered);
}
[Fact]
public void IdenticalOrStaleState_DoesNotReplayHiddenEffect()
{
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
fixture.Controller.OnLiveEntityReady(Fixture.Guid);
var hidden = new SetState.Parsed(
Fixture.Guid,
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions),
1,
2);
Assert.True(fixture.Runtime.TryApplyState(hidden, out _, out _));
fixture.Controller.OnStateAccepted(Fixture.Guid);
Assert.False(fixture.Runtime.TryApplyState(hidden, out _, out _));
fixture.Controller.OnStateAccepted(Fixture.Guid);
Assert.Single(fixture.TypedPlays);
Assert.Equal(LiveEntityPresentationController.HiddenScriptType,
fixture.TypedPlays[0].Type);
Assert.Equal([fixture.Entity.Id], fixture.PartArrayEnterWorld);
}
[Fact]
public void StateAcceptedDuringHiddenEffect_DrainsAfterCompleteHiddenTail()
{
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
bool nestedAccepted = false;
fixture.OnTypedPlay = type =>
{
if (type != LiveEntityPresentationController.HiddenScriptType
|| nestedAccepted)
{
return;
}
nestedAccepted = true;
Assert.True(fixture.Runtime.TryApplyState(
new SetState.Parsed(
Fixture.Guid,
(uint)PhysicsStateFlags.ReportCollisions,
1,
3),
out _));
Assert.True(fixture.Controller.OnStateAccepted(Fixture.Guid));
};
Assert.True(fixture.Runtime.TryApplyState(
new SetState.Parsed(
Fixture.Guid,
(uint)(PhysicsStateFlags.Hidden
| PhysicsStateFlags.ReportCollisions),
1,
2),
out _));
Assert.True(fixture.Controller.OnStateAccepted(Fixture.Guid));
Assert.True(nestedAccepted);
Assert.Equal(
[
"effect:76", "children:True", "part-array", "target",
"effect:75", "children:False", "part-array",
],
fixture.PresentationOrder);
Assert.True(fixture.Entity.IsDrawVisible);
Assert.Equal(1, fixture.Shadows.TotalRegistered);
}
[Fact]
public void DeleteAndGuidReuseDuringHiddenEffect_StopsOldTransitionTail()
{
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
Assert.True(fixture.Runtime.TryGetRecord(
Fixture.Guid,
out LiveEntityRecord oldRecord));
WorldEntity? replacement = null;
fixture.OnTypedPlay = type =>
{
if (type != LiveEntityPresentationController.HiddenScriptType
|| replacement is not null)
{
return;
}
fixture.Controller.Forget(oldRecord);
fixture.Shadows.Deregister(fixture.Entity.Id);
Assert.True(fixture.Runtime.UnregisterLiveEntity(
new DeleteObject.Parsed(Fixture.Guid, 1),
isLocalPlayer: false));
fixture.Runtime.RegisterLiveEntity(Fixture.Spawn(
PhysicsStateFlags.ReportCollisions,
instanceSequence: 2));
replacement = fixture.Runtime.MaterializeLiveEntity(
Fixture.Guid,
0x01010001u,
id => new WorldEntity
{
Id = id,
ServerGuid = Fixture.Guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(20f, 20f, 5f),
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
});
Assert.NotNull(replacement);
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
};
Assert.True(fixture.Runtime.TryApplyState(
new SetState.Parsed(
Fixture.Guid,
(uint)(PhysicsStateFlags.Hidden
| PhysicsStateFlags.ReportCollisions),
1,
2),
out _));
Assert.False(fixture.Controller.OnStateAccepted(Fixture.Guid));
Assert.NotNull(replacement);
Assert.Equal(["effect:76"], fixture.PresentationOrder);
Assert.Empty(fixture.ChildNoDraw);
Assert.Empty(fixture.PartArrayEnterWorld);
Assert.Empty(fixture.InvalidTargets);
Assert.True(fixture.Runtime.TryGetRecord(Fixture.Guid, out var current));
Assert.Same(replacement, current.WorldEntity);
Assert.NotSame(oldRecord, current);
}
[Fact]
public void UnHideWhileLandblockPending_RestoresShadowWhenProjectionReturns()
{
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
fixture.Controller.OnLiveEntityReady(Fixture.Guid);
Assert.True(fixture.Runtime.TryApplyState(
new SetState.Parsed(
Fixture.Guid,
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions),
1,
2),
out _,
out _));
fixture.Controller.OnStateAccepted(Fixture.Guid);
Assert.Equal(0, fixture.Shadows.TotalRegistered);
Assert.True(fixture.Runtime.RebucketLiveEntity(Fixture.Guid, 0x02020001u));
Assert.False(fixture.Runtime.TryGetInteractionEligibleEntity(
Fixture.Guid,
out _));
Assert.True(fixture.Runtime.TryApplyState(
new SetState.Parsed(Fixture.Guid, 0u, 1, 3),
out _,
out _));
fixture.Controller.OnStateAccepted(Fixture.Guid);
Assert.Equal(0, fixture.Shadows.TotalRegistered);
fixture.Spatial.AddLandblock(new LoadedLandblock(
0x0202FFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
Assert.True(fixture.Runtime.TryGetInteractionEligibleEntity(
Fixture.Guid,
out _));
Assert.Equal(1, fixture.Shadows.TotalRegistered);
}
[Fact]
public void VisibleOrdinaryProjectionPending_SuspendsAndHydrationRestoresShadow()
{
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
Assert.Equal(1, fixture.Shadows.TotalRegistered);
Assert.True(fixture.Runtime.RebucketLiveEntity(
Fixture.Guid,
0x02020001u));
Assert.False(fixture.Runtime.TryGetInteractionEligibleEntity(
Fixture.Guid,
out _));
Assert.Equal(0, fixture.Shadows.TotalRegistered);
Assert.Equal(1, fixture.Shadows.RetainedRegistrationCount);
Assert.Equal(1, fixture.Shadows.SuspendedRegistrationCount);
Assert.True(fixture.Controller.HasDeferredShadowRestore(Fixture.Guid));
fixture.Spatial.AddLandblock(new LoadedLandblock(
0x0202FFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
Assert.Same(
fixture.Entity,
Assert.Single(fixture.Runtime.VisibleRecords).WorldEntity);
Assert.Equal(1, fixture.Shadows.TotalRegistered);
Assert.Equal(0, fixture.Shadows.SuspendedRegistrationCount);
Assert.False(fixture.Controller.HasDeferredShadowRestore(Fixture.Guid));
}
[Fact]
public void PendingVisibilityCallback_GuidReuseCannotRestoreOldShadowOwner()
{
Fixture fixture = new(PhysicsStateFlags.ReportCollisions);
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
Assert.True(fixture.Runtime.TryGetRecord(
Fixture.Guid,
out LiveEntityRecord oldRecord));
WorldEntity oldEntity = fixture.Entity;
WorldEntity? replacement = null;
fixture.Runtime.ProjectionVisibilityChanged += (record, visible) =>
{
if (visible
|| replacement is not null
|| !ReferenceEquals(record, oldRecord))
{
return;
}
fixture.Controller.Forget(oldRecord);
fixture.Shadows.Deregister(oldEntity.Id);
Assert.True(fixture.Runtime.UnregisterLiveEntity(
new DeleteObject.Parsed(Fixture.Guid, 1),
isLocalPlayer: false));
fixture.Runtime.RegisterLiveEntity(Fixture.Spawn(
PhysicsStateFlags.ReportCollisions,
instanceSequence: 2));
replacement = fixture.Runtime.MaterializeLiveEntity(
Fixture.Guid,
0x01010001u,
id => new WorldEntity
{
Id = id,
ServerGuid = Fixture.Guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(20f, 20f, 5f),
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
});
Assert.NotNull(replacement);
fixture.Shadows.Register(
replacement.Id,
0x01000001u,
replacement.Position,
replacement.Rotation,
1f,
0f,
0f,
0x01010000u,
state: (uint)PhysicsStateFlags.ReportCollisions,
seedCellId: 0x01010001u);
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
};
Assert.False(fixture.Runtime.RebucketLiveEntity(
Fixture.Guid,
0x02020001u));
Assert.NotNull(replacement);
Assert.True(fixture.Runtime.TryGetRecord(Fixture.Guid, out var current));
Assert.NotSame(oldRecord, current);
Assert.Same(replacement, current.WorldEntity);
Assert.Equal(1, fixture.Shadows.TotalRegistered);
Assert.Equal(1, fixture.Shadows.RetainedRegistrationCount);
Assert.Equal(0, fixture.Shadows.SuspendedRegistrationCount);
Assert.Contains(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == replacement.Id);
Assert.DoesNotContain(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == oldEntity.Id);
Assert.False(fixture.Controller.HasDeferredShadowRestore(Fixture.Guid));
}
[Fact]
public void InitialPendingProjection_ReadyBarrierSuspendsAndHydrationRestoresShadow()
{
const uint pendingCell = 0x02020001u;
var spatial = new GpuWorldState();
var shadows = new ShadowObjectRegistry();
var runtime = LiveEntityRuntimeFixture.Create(
spatial,
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
runtime.RegisterLiveEntity(Fixture.Spawn(
PhysicsStateFlags.ReportCollisions));
WorldEntity entity = Assert.IsType<WorldEntity>(
runtime.MaterializeLiveEntity(
Fixture.Guid,
pendingCell,
id => new WorldEntity
{
Id = id,
ServerGuid = Fixture.Guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(10f, 10f, 5f),
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = pendingCell,
}));
shadows.Register(
entity.Id,
0x01000001u,
entity.Position,
entity.Rotation,
1f,
0f,
0f,
0x02020000u,
state: (uint)PhysicsStateFlags.ReportCollisions,
seedCellId: pendingCell);
using var controller = new LiveEntityPresentationController(
runtime,
shadows,
(_, _, _) => true,
new LiveEntityPartArrayEnterWorldPort(_ => { }),
liveCenter: () => (2, 2));
Assert.True(controller.OnLiveEntityReady(Fixture.Guid));
Assert.Equal(0, shadows.TotalRegistered);
Assert.Equal(1, shadows.RetainedRegistrationCount);
Assert.Equal(1, shadows.SuspendedRegistrationCount);
Assert.True(controller.HasDeferredShadowRestore(Fixture.Guid));
spatial.AddLandblock(new LoadedLandblock(
0x0202FFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
Assert.Same(entity, Assert.Single(runtime.VisibleRecords).WorldEntity);
Assert.Equal(1, shadows.TotalRegistered);
Assert.Equal(0, shadows.SuspendedRegistrationCount);
Assert.False(controller.HasDeferredShadowRestore(Fixture.Guid));
}
[Fact]
public void InitialPendingHydrationCallback_GuidReuseCannotRestoreOldShadow()
{
const uint pendingCell = 0x02020001u;
var spatial = new GpuWorldState();
var shadows = new ShadowObjectRegistry();
var runtime = LiveEntityRuntimeFixture.Create(
spatial,
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
runtime.RegisterLiveEntity(Fixture.Spawn(
PhysicsStateFlags.ReportCollisions));
WorldEntity oldEntity = Assert.IsType<WorldEntity>(
runtime.MaterializeLiveEntity(
Fixture.Guid,
pendingCell,
id => new WorldEntity
{
Id = id,
ServerGuid = Fixture.Guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(10f, 10f, 5f),
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = pendingCell,
}));
Assert.True(runtime.TryGetRecord(Fixture.Guid, out LiveEntityRecord oldRecord));
shadows.Register(
oldEntity.Id,
0x01000001u,
oldEntity.Position,
oldEntity.Rotation,
1f,
0f,
0f,
0x02020000u,
state: (uint)PhysicsStateFlags.ReportCollisions,
seedCellId: pendingCell);
LiveEntityPresentationController? controller = null;
WorldEntity? replacement = null;
runtime.ProjectionVisibilityChanged += (record, visible) =>
{
if (!visible
|| replacement is not null
|| !ReferenceEquals(record, oldRecord))
{
return;
}
controller!.Forget(oldRecord);
shadows.Deregister(oldEntity.Id);
Assert.True(runtime.UnregisterLiveEntity(
new DeleteObject.Parsed(Fixture.Guid, 1),
isLocalPlayer: false));
runtime.RegisterLiveEntity(Fixture.Spawn(
PhysicsStateFlags.ReportCollisions,
instanceSequence: 2));
replacement = runtime.MaterializeLiveEntity(
Fixture.Guid,
pendingCell,
id => new WorldEntity
{
Id = id,
ServerGuid = Fixture.Guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(20f, 20f, 5f),
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = pendingCell,
});
Assert.NotNull(replacement);
shadows.Register(
replacement.Id,
0x01000001u,
replacement.Position,
replacement.Rotation,
1f,
0f,
0f,
0x02020000u,
state: (uint)PhysicsStateFlags.ReportCollisions,
seedCellId: pendingCell);
Assert.True(controller.OnLiveEntityReady(Fixture.Guid));
};
controller = new LiveEntityPresentationController(
runtime,
shadows,
(_, _, _) => true,
new LiveEntityPartArrayEnterWorldPort(_ => { }),
liveCenter: () => (2, 2));
Assert.True(controller.OnLiveEntityReady(Fixture.Guid));
Assert.Equal(1, shadows.SuspendedRegistrationCount);
spatial.AddLandblock(new LoadedLandblock(
0x0202FFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
Assert.NotNull(replacement);
Assert.True(runtime.TryGetRecord(Fixture.Guid, out var current));
Assert.NotSame(oldRecord, current);
Assert.Same(replacement, current.WorldEntity);
Assert.Equal(1, shadows.TotalRegistered);
Assert.Equal(1, shadows.RetainedRegistrationCount);
Assert.Equal(0, shadows.SuspendedRegistrationCount);
Assert.Contains(
shadows.AllEntriesForDebug(),
entry => entry.EntityId == replacement.Id);
Assert.DoesNotContain(
shadows.AllEntriesForDebug(),
entry => entry.EntityId == oldEntity.Id);
Assert.False(controller.HasDeferredShadowRestore(Fixture.Guid));
controller.Dispose();
}
// C4 route 4b-3: ActivePlacement_IsGenerationScopedAndClearsOnTeardownAndReset
// deleted — BeginAuthoritativePlacement/CompleteAuthoritativePlacement/
// HasActivePlacement/DeferShadowRestore and the backing
// _activePlacementOwners set are dead (their only production caller was
// the deleted RemoteTeleportPlacementPresentation); see
// docs/research/2026-08-04-c4-route-4b-3-contract.md's deletion inventory.
private sealed class Fixture
{
public const uint Guid = 0x70000051u;
public List<(uint Owner, uint Type, float Intensity)> TypedPlays { get; } = [];
public List<(uint Parent, bool NoDraw)> ChildNoDraw { get; } = [];
public List<uint> InvalidTargets { get; } = [];
public List<uint> PartArrayEnterWorld { get; } = [];
public List<int> PartArrayShadowCounts { get; } = [];
public List<string> PresentationOrder { get; } = [];
public GpuWorldState Spatial { get; }
public LiveEntityRuntime Runtime { get; }
public ShadowObjectRegistry Shadows { get; } = new();
public WorldEntity Entity { get; }
public LiveEntityPresentationController Controller { get; }
public Action<uint>? OnTypedPlay { get; set; }
public Fixture(
PhysicsStateFlags initialState,
Action<uint>? onPartArrayEnterWorld = null)
{
Spatial = new GpuWorldState();
Spatial.AddLandblock(new LoadedLandblock(
0x0101FFFFu,
new LandBlock(),
Array.Empty<WorldEntity>()));
Runtime = LiveEntityRuntimeFixture.Create(
Spatial,
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }));
WorldSession.EntitySpawn spawn = Spawn(initialState);
Runtime.RegisterLiveEntity(spawn);
Entity = Runtime.MaterializeLiveEntity(
Guid,
0x01010001u,
id => new WorldEntity
{
Id = id,
ServerGuid = Guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = new Vector3(10f, 10f, 5f),
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
Shadows.Register(
Entity.Id,
0x01000001u,
Entity.Position,
Entity.Rotation,
1f,
0f,
0f,
0x01010000u,
state: (uint)initialState,
seedCellId: 0x01010001u);
Controller = new LiveEntityPresentationController(
Runtime,
Shadows,
(owner, type, intensity) =>
{
TypedPlays.Add((owner, type, intensity));
PresentationOrder.Add($"effect:{type:X2}");
OnTypedPlay?.Invoke(type);
return true;
},
new LiveEntityPartArrayEnterWorldPort(localEntityId =>
{
PartArrayEnterWorld.Add(localEntityId);
PartArrayShadowCounts.Add(Shadows.TotalRegistered);
PresentationOrder.Add("part-array");
onPartArrayEnterWorld?.Invoke(localEntityId);
}),
(parent, noDraw) =>
{
ChildNoDraw.Add((parent, noDraw));
PresentationOrder.Add($"children:{noDraw}");
},
guid =>
{
InvalidTargets.Add(guid);
PresentationOrder.Add("target");
},
() => (1, 1),
onShadowRestored: null);
}
internal static WorldSession.EntitySpawn Spawn(
PhysicsStateFlags state,
ushort instanceSequence = 1)
{
var position = new CreateObject.ServerPosition(
0x01010001u, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
// C3c: residence admission requires the nested block's Instance
// timestamp to agree with the flattened InstanceSequence.
var timestamps = new PhysicsTimestamps(
1, 1, 1, 1, 0, 1, 0, 1, instanceSequence);
var physics = new PhysicsSpawnData(
RawState: (uint)state,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
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: timestamps);
return new WorldSession.EntitySpawn(
Guid,
position,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
null,
null,
"fixture",
null,
null,
MotionTableId: 0x09000001u,
PhysicsState: (uint)state,
InstanceSequence: instanceSequence,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
}
}