using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using AcDream.App.Input;
using AcDream.App.Net;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Vfx;
using AcDream.App.Streaming;
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Content;
using AcDream.Content.Pak;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using DatReaderWriter.Lib.IO;
namespace AcDream.App.Tests.Physics;
///
/// C4 route 4b-3, test-plan item 9 (the #312 layer, strong form): drives the
/// COMPLETE production entry point — a real
/// call, not the
/// extracted
/// seam the far-snap/steady-state integration tests use — for a remote
/// teleport, and asserts the render layer directly: the
/// pose, ParentCellId, spatial visibility, and collision-shadow
/// publication. #312 shipped because a presentation-restore fix's tests
/// asserted only InWorld/clock/residency, never this layer — these
/// tests exist so the same class of regression cannot ship silently for the
/// teleport arm.
///
///
/// Every dependency is a REAL production class except the ~15 interface
/// seams LiveEntityNetworkUpdateController takes for local-player-only
/// or DAT-backed concerns that a remote-teleport call never reaches (verified
/// by reading OnPosition: _dats/_animLoader are read only
/// from OnMotion; _localPlayerTeleport/_playerHostSource/
/// _acceptedPositionDrive are gated on update.Guid ==
/// _playerServerGuid, which the remote guid here never satisfies). The
/// canonical is shared between
/// and ,
/// exactly like production's SessionPlayerComposition wires them.
///
///
///
/// deliberately does NOT satisfy
/// IsPlayerGuid ((guid & 0xFF000000u) == 0x50000000u), so
/// OnPosition takes the "GROUNDED ROUTING" creature/NPC branch and
/// its own render+shadow tail (the block after the
/// #184: sync the NPC shadow... comment), not the sibling copy inside
/// the IsPlayerGuid branch above it — the two are separate inline
/// copies of the same invariant, not a shared helper. Verified live: a
/// sabotage of the wrong (player-guid) copy left both tests green, which is
/// what caught the branch mismatch during authoring; sabotaging the actual
/// NPC-branch copy fails both tests as expected.
///
///
public sealed class LiveEntityNetworkRemoteTeleportPresentationTests
{
private const uint SourceLandblock = 0xB1000000u;
private const uint SourceCell = SourceLandblock | 0x0001u;
private const uint DestinationLandblock = 0xB2000000u;
private const uint DestinationCell = DestinationLandblock | 0x0001u;
private static readonly Vector3 DestinationWorldOffset = new(192f, 0f, 0f);
private const uint RemoteGuid = 0x70006001u;
///
/// In IsPlayerGuid's 0x50xxxxxx range but distinct from
/// the fixture's own NoopIdentitySource.ServerGuid
/// (0x50000099u) — an OTHER player's remote character, exactly
/// the guid shape that takes OnPosition's IsPlayerGuid
/// branch (and its own airborne "landing block" early return) while
/// still classifying as a remote (update.Guid != _playerServerGuid).
///
private const uint OtherPlayerGuid = 0x50006001u;
private const float SpawnHeight = 7f;
///
/// Retail's foot-sphere convention (PhysicsEngine.Resolve's
/// FootSphereCenterLift, #107 2026-06-10): a body's Z sits this
/// far above the terrain sample its foot sphere rests on, center-to-
/// contact. A canonical placement onto flat terrain therefore resolves
/// to terrainHeight + FootSphereCenterLift, not the raw wire Z.
///
private const float FootSphereCenterLift = 0.48f;
///
/// The strong form of test-plan item 9's commit half: after a remote
/// teleport commits, the render pose equals the
/// resolved body pose, ParentCellId equals the resolved cell, the
/// entity is spatially visible, and the collision shadow was published
/// (re-synced) at the resolved position — not merely registered once at
/// spawn. Fails against a broken implementation that skips the teleport
/// arm's render/shadow tail (invariant 2): reverting that tail to a
/// no-op leaves Entity.Position at the pre-teleport spawn pose and
/// the shadow registry's position unsynced, both asserted below.
///
[Fact]
public void TeleportCommit_RenderEntityMatchesResolvedBody_VisibleAndShadowSynced()
{
using var fixture = new Fixture();
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
Vector3 spawnPose = fixture.Entity.Position;
var destination = new Vector3(12f, 14f, SpawnHeight);
fixture.Controller.OnPosition(fixture.TeleportUpdate(
destination, DestinationCell, teleportSequence: 5));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
RemoteGuid, out RuntimeEntityRecord canonical));
Assert.NotNull(canonical.PhysicsBody);
PhysicsBody body = canonical.PhysicsBody!;
Vector3 resolved = destination + DestinationWorldOffset
+ new Vector3(0f, 0f, FootSphereCenterLift);
Assert.Equal(resolved, body.Position);
Assert.NotEqual(spawnPose, body.Position);
// Invariant 2: the render entity advances from the RESOLVED body.
Assert.Equal(body.Position, fixture.Entity.Position);
Assert.Equal(body.Orientation, fixture.Entity.Rotation);
Assert.Equal(DestinationCell, fixture.Entity.ParentCellId);
Assert.Equal(DestinationCell, canonical.FullCellId);
Assert.True(fixture.Runtime.TryGetRecord(
RemoteGuid, out LiveEntityRecord liveRecord));
Assert.True(liveRecord.IsSpatiallyVisible);
// The collision shadow was RE-PUBLISHED at the resolved position —
// not merely the stale spawn-time registration.
AcDream.Core.Physics.ShadowEntry shadowEntry = Assert.Single(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == fixture.Entity.Id);
Assert.Equal(body.Position, shadowEntry.Position);
fixture.DrainPlacementFifo();
}
///
/// Test-plan item 9's refused half: invariant 1 (the pose still
/// advances) restated at the render layer, plus the #312-specific claim
/// — the entity REMAINS visible. Fails against a regression that leaves
/// the render entity at its stale pose or withdraws it from the spatial
/// working set on a refusal.
///
///
/// A4 fix round (2026-08-04): every prior assertion here was ALSO
/// satisfied by a regression that misclassified this packet as
/// UnroutedCatchUp instead of the teleport arm — its
/// ApplyInterpolate hard-place lands the body at the exact same
/// wire pose (AP-87's bodyToTarget > 4 m branch) and the SAME
/// NPC tail then syncs entity/shadow identically, so none of those
/// assertions could tell the two arms apart. Arming sticky BEFORE the
/// packet and asserting it was cleared afterward closes that gap:
/// UnroutedCatchUp never reaches ApplyRemoteContactRouting's
/// teleport branch at all, so it never runs the hook, so a stuck NPC
/// misclassified onto that arm would stay stuck — only the real teleport
/// arm's hook calls UnStick.
///
///
[Fact]
public void TeleportRefused_RenderPoseTracksStoredDestination_EntityRemainsVisible()
{
using var fixture = new Fixture();
fixture.PublishDestinationCollision();
// Deliberately NOT ServiceWindow.Allow(DestinationLandblock) — the
// canonical placement refuses, so store_position's fallback is what
// must move the body (and the render entity behind it).
Vector3 spawnPose = fixture.Entity.Position;
var destination = new Vector3(12f, 14f, SpawnHeight);
EntityPhysicsHost host = fixture.ArmSticky(stickTargetGuid: 0x70009998u);
fixture.Controller.OnPosition(fixture.TeleportUpdate(
destination, DestinationCell, teleportSequence: 5));
// The discriminator: only the real teleport arm's hook clears this.
Assert.Equal(0u, host.PositionManager.GetStickyObjectId());
Assert.True(fixture.Lifetime.Entities.TryGetActive(
RemoteGuid, out RuntimeEntityRecord canonical));
PhysicsBody body = canonical.PhysicsBody!;
// Unlike the commit half, a REFUSED placement never runs the
// ground-contact sweep that applies FootSphereCenterLift — the
// store_position fallback stamps the raw wire position verbatim.
Vector3 resolved = destination + DestinationWorldOffset;
Assert.Equal(resolved, body.Position);
Assert.NotEqual(spawnPose, body.Position);
Assert.Equal(body.Position, fixture.Entity.Position);
Assert.True(fixture.Runtime.TryGetRecord(
RemoteGuid, out LiveEntityRecord liveRecord));
Assert.True(liveRecord.IsSpatiallyVisible);
// The collision shadow was RE-PUBLISHED at the stored destination —
// the generic top-of-OnPosition wire-pose write never touches the
// shadow registry, so this specifically exercises the tail.
AcDream.Core.Physics.ShadowEntry shadowEntry = Assert.Single(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == fixture.Entity.Id);
Assert.Equal(body.Position, shadowEntry.Position);
fixture.DrainPlacementFifo();
}
///
/// Test-plan item 6 / contract D5: TS-44's sticky suppression is an
/// NPC-only CALLER gate that must NOT suppress the teleport arm — retail
/// sticky cannot survive a teleport (UnStick is
/// teleport_hook's second action, @0x00514EEE). The exact gate
/// under test is
/// LiveEntityNetworkUpdateController.OnPosition's
/// if (!snapSuppressedByStick || isTeleportRoute) — a stuck NPC's
/// teleport packet must still run the hook (observable here as
/// UnStick actually clearing the sticky lease) and place
/// canonically, exactly as an unstuck NPC's would.
///
[Fact]
public void StuckNpc_TeleportPacket_RunsTheHookAndPlacesDespiteStickySuppression()
{
using var fixture = new Fixture();
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
EntityPhysicsHost host = fixture.ArmSticky(stickTargetGuid: 0x70009999u);
var destination = new Vector3(12f, 14f, SpawnHeight);
fixture.Controller.OnPosition(fixture.TeleportUpdate(
destination, DestinationCell, teleportSequence: 5));
// The hook's UnStick action actually ran — proof the sticky lease
// did not block dispatch, not merely that the placement happened to
// succeed for some unrelated reason.
Assert.Equal(0u, host.PositionManager.GetStickyObjectId());
Assert.True(fixture.Lifetime.Entities.TryGetActive(
RemoteGuid, out RuntimeEntityRecord canonical));
Assert.NotNull(canonical.PhysicsBody);
PhysicsBody body = canonical.PhysicsBody!;
Vector3 resolved = destination + DestinationWorldOffset
+ new Vector3(0f, 0f, FootSphereCenterLift);
Assert.Equal(resolved, body.Position);
Assert.Equal(DestinationCell, canonical.FullCellId);
fixture.DrainPlacementFifo();
}
///
/// Test-plan item 6 / contract D5, the OnPosition-level restatement:
/// "the player arm's landing block (!rmState.Body.InContact
/// hard-snap + return) must not claim a teleport-classified packet" and
/// "the 4a IsAirborneNoOperation early returns... cannot claim a
/// SetPosition route". takes
/// OnPosition's IsPlayerGuid branch (a DIFFERENT code path
/// than every other test in this file, which use the creature-guid
/// branch) — an ungrounded wire packet (IsGrounded: false) for a
/// teleport-classified body must still reach the canonical placement
/// through that branch's own copy of ApplyRemoteContactRouting,
/// not the branch's airborne early return at the top of the
/// IsPlayerGuid block.
///
[Fact]
public void AirborneOtherPlayer_TeleportPacket_PlacesThroughThePlayerArmWithoutTheLandingBlock()
{
using var fixture = new Fixture(OtherPlayerGuid);
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
// Same mid-arc state the routing-seam test above uses: no wire
// contact, no body contact — the strongest form of "airborne".
fixture.Remote.Airborne = true;
fixture.Remote.Body.TransientState = TransientStateFlags.Active;
var destination = new Vector3(12f, 14f, SpawnHeight);
fixture.Controller.OnPosition(fixture.TeleportUpdate(
destination,
DestinationCell,
teleportSequence: 5,
guid: OtherPlayerGuid,
isGrounded: false));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
OtherPlayerGuid, out RuntimeEntityRecord canonical));
Assert.NotNull(canonical.PhysicsBody);
PhysicsBody body = canonical.PhysicsBody!;
Vector3 resolved = destination + DestinationWorldOffset
+ new Vector3(0f, 0f, FootSphereCenterLift);
Assert.Equal(resolved, body.Position);
Assert.Equal(DestinationCell, canonical.FullCellId);
// The airborne early return writes NOTHING and returns before the
// spatial rebucket that follows a real dispatch — resident
// visibility is proof OnPosition did not take that exit.
Assert.True(fixture.Runtime.TryGetRecord(
OtherPlayerGuid, out LiveEntityRecord liveRecord));
Assert.True(liveRecord.IsSpatiallyVisible);
fixture.DrainPlacementFifo();
}
///
/// A1 fix round (2026-08-04): the NPC arm must still arm the leash for
/// an ordinary landing packet — wire IS grounded (retail's
/// arg4 != 0), body is NOT in contact (acdream's own
/// snap-vs-interpolate axis, unrelated to retail's arming predicate).
/// 's
/// free-flight carve-out claims this as AirborneSnap regardless of
/// classification (near/far/leftover would all otherwise apply); before
/// the fix, ToConstraintArm mapped AirborneSnap to the one
/// Runtime value that never arms, so this exact packet — a creature
/// knocked off a ledge — armed ZERO times where retail's
/// MoveOrTeleport returns nonzero and arms unconditionally
/// @0x00454272. host.PositionManager.Constraint is lazily created
/// only on a genuine arm, so its presence after the packet is direct,
/// unambiguous proof of the fix — not an inference from body position.
///
[Fact]
public void NpcAirborneSnap_LandingPacket_StillArmsTheLeash()
{
using var fixture = new Fixture();
EntityPhysicsHost host = fixture.InstallHost();
Assert.Null(host.PositionManager.Constraint);
// Mid-arc: body not in contact, wire IS grounded (this packet is an
// ordinary landing correction, not a free-flight UP).
fixture.Remote.Airborne = true;
fixture.Remote.Body.TransientState = TransientStateFlags.Active;
var landingPos = new Vector3(12f, 14f, SpawnHeight);
fixture.Controller.OnPosition(fixture.TeleportUpdate(
landingPos, SourceCell, teleportSequence: 1, isGrounded: true));
Assert.NotNull(host.PositionManager.Constraint);
}
///
/// R3/A2 fix round (2026-08-04): a teleported NPC must not synthesize a
/// locomotion velocity from the teleport distance. Before the fix, the
/// deleted shared remotePlacementRequired block used to sit ABOVE
/// the NPC synth-velocity code and always returned, so a teleport packet
/// never reached it; once that block was deleted and the teleport arm
/// routed through the NPC tail, the synth-velocity install ran
/// unconditionally, computing (worldPos - LastServerPos) / elapsed
/// across the WHOLE teleport distance over one packet interval —
/// hundreds to thousands of m/s — and installing it as
/// ServerVelocity. 's
/// LastServerPos/LastServerPosTime are seeded to simulate
/// an already-tracked creature (the real @teleto-on-a-visible-
/// drudge scenario) so a broken implementation's synth would have a real
/// distance/interval to compute from, not a degenerate first-packet
/// no-op.
///
[Fact]
public void NpcTeleport_DoesNotInstallASynthesizedVelocity()
{
using var fixture = new Fixture();
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
fixture.Remote.LastServerPos = fixture.Entity.Position;
fixture.Remote.LastServerPosTime =
(DateTime.UtcNow - DateTime.UnixEpoch).TotalSeconds - 0.15;
var destination = new Vector3(12f, 14f, SpawnHeight);
fixture.Controller.OnPosition(fixture.TeleportUpdate(
destination, DestinationCell, teleportSequence: 5));
Assert.False(fixture.Remote.HasServerVelocity);
Assert.Equal(Vector3.Zero, fixture.Remote.ServerVelocity);
fixture.DrainPlacementFifo();
}
///
/// Test-plan item 10 (R1/A7 fix round, 2026-08-04): "a wire-airborne
/// null-classified packet writes exactly AP-135's two fields and nothing
/// else (body, entity, queue, leash all untouched)" — driven for the NPC
/// arm specifically, which shipped WITHOUT this D2 shape (the player arm
/// had it from the start). Before the fix, a wire-airborne, null/
/// Rejected*-classified NPC packet fell through to
/// 's
/// own free-flight carve-out and received a hard-snap body write, a
/// leash arm, and a shadow republish — none of which retail's
/// return 0 (@0x0051636D) produces.
/// forces the null-classification shape (no local player exists yet to
/// supply player_distance — the login-window case D2 is scoped
/// to); teleportSequence: 1 matches the spawn's own committed
/// teleport timestamp so this packet does NOT advance TELEPORT_TS
/// (never teleport-classified, even setting classification aside).
///
[Fact]
public void NullClassifiedNpc_WireAirbornePacket_WritesOnlyBookkeepingNoBodyOrShadow()
{
using var fixture = new Fixture(nullClassification: true);
EntityPhysicsHost host = fixture.InstallHost();
Assert.Null(host.PositionManager.Constraint);
Vector3 spawnBodyPose = fixture.Remote.Body.Position;
var wirePos = new Vector3(50f, 50f, SpawnHeight);
fixture.Controller.OnPosition(fixture.TeleportUpdate(
wirePos,
SourceCell,
teleportSequence: 1,
isGrounded: false));
// D2's core claim: no body write. A broken implementation that lets
// this packet reach ApplyRemoteContactRouting's free-flight carve-out
// hard-snaps the body to wirePos here.
Assert.Equal(spawnBodyPose, fixture.Remote.Body.Position);
// No shadow republish — still exactly the one spawn-time entry, at
// the spawn pose, not wirePos.
AcDream.Core.Physics.ShadowEntry shadowEntry = Assert.Single(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == fixture.Entity.Id);
Assert.Equal(spawnBodyPose, shadowEntry.Position);
// Round-2 architecture review B1: everything above asserts only what
// must NOT happen, so an EMPTIED ApplyWireAirborneLeftoverBookkeeping
// passed every test in the tree. AP-135's two writes are the POSITIVE
// half of D2's shape and are load-bearing: RemoteMotion.CellId feeds
// RuntimeRemotePhysicsUpdater's free-fall sweep gate (its `rm.CellId
// != 0` test) and the LastServerPos/LastServerPosTime pair seeds the
// first-grounded-packet velocity synthesis. Losing them silently is
// the "airborne remote falls through the floor" shape.
Assert.Equal(SourceCell, fixture.Remote.CellId);
Assert.Equal(wirePos, fixture.Remote.LastServerPos);
Assert.NotEqual(0d, fixture.Remote.LastServerPosTime);
// Round-2 retail review: this test's name and doc both claim the leash
// is untouched — a D4 "no arm" row, because retail returns 0
// @0x0051636D and never reaches ConstrainTo @0x00454272 — but nothing
// asserted it. The constraint sub-manager is lazily created only
// inside a genuine arm, so null is direct proof of zero arming calls.
Assert.Null(host.PositionManager.Constraint);
}
private sealed class Fixture : IDisposable
{
internal RuntimeEntityObjectLifetime Lifetime { get; }
internal LiveEntityRuntime Runtime { get; }
internal LiveEntityNetworkUpdateController Controller { get; }
internal RemoteServiceWindow ServiceWindow { get; } = new();
internal ShadowObjectRegistry Shadows { get; }
internal WorldEntity Entity { get; }
internal RemoteMotion Remote { get; private set; } = null!;
private readonly GpuWorldState _spatial;
private readonly uint _guid;
private readonly bool _nullClassification;
///
/// Defaults to (an ordinary creature-style
/// guid, NOT in IsPlayerGuid's 0x50xxxxxx range).
/// Tests that need the OTHER production remote branch — the
/// IsPlayerGuid(update.Guid) block in OnPosition, which
/// has its own airborne early return (the "player arm's landing
/// block") — pass a guid in that range instead.
///
internal Fixture(uint guid = RemoteGuid, bool nullClassification = false)
{
_guid = guid;
_nullClassification = nullClassification;
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
engine.AddLandblock(
SourceLandblock,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty(),
Array.Empty(),
worldOffsetX: 0f,
worldOffsetY: 0f);
Lifetime = new RuntimeEntityObjectLifetime(engine);
Lifetime.BindEventContext(
static () => new RuntimeGenerationToken(1UL),
static () => 1UL);
Shadows = engine.ShadowObjects;
var spatial = new GpuWorldState();
// GpuWorldState's own bucket key is the CANONICAL landblock form
// ((id & 0xFFFF0000) | 0xFFFF) — the same form
// RebucketLiveEntity/RuntimeEntityObjectLifetime derive from a
// cell id. Registering the raw 0x....0000 id here would silently
// leave the entity in the PENDING bucket forever.
spatial.AddLandblock(new LoadedLandblock(
CanonicalLandblock(SourceLandblock),
new DatReaderWriter.DBObjs.LandBlock(),
Array.Empty()));
_spatial = spatial;
Runtime = new LiveEntityRuntime(
spatial,
new NoopResources(),
NullLiveEntityRuntimeComponentLifecycle.Instance,
Lifetime);
var wirePosition = new CreateObject.ServerPosition(
SourceCell, 10f, 10f, SpawnHeight, 1f, 0f, 0f, 0f);
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 1,
ServerControlledMove: 1,
ForcePosition: 1,
ObjDesc: 1,
Instance: 1);
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.ReportCollisions,
Position: wirePosition,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: 1f,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
var spawn = new WorldSession.EntitySpawn(
_guid,
wirePosition,
0x02000001u,
Array.Empty(),
Array.Empty(),
Array.Empty(),
null,
null,
"remote-teleport-fixture",
null,
null,
0x09000001u,
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
InstanceSequence: 1,
PositionSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
Physics: physics);
LiveEntityRecord record =
Runtime.RegisterAndMaterializeProjection(spawn);
Entity = record.WorldEntity
?? throw new InvalidOperationException(
"fixture failed to materialize the remote entity");
Assert.True(Runtime.RebucketLiveEntity(_guid, SourceCell));
// Adopts this body as the canonical PhysicsBody (none exists yet)
// — mirrors LiveEntityLifecycleStressTests' RecallPortalFixture.
var remote = new RemoteMotion();
remote.Body.SnapToCell(SourceCell, Entity.Position, Entity.Position);
remote.CellId = SourceCell;
Runtime.SetRemoteMotionRuntime(_guid, remote);
Remote = remote;
Shadows.Register(
Entity.Id,
0x02000001u,
Entity.Position,
Entity.Rotation,
radius: 0.48f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: SourceLandblock,
collisionType: ShadowCollisionType.Cylinder,
cylHeight: 1.835f,
seedCellId: SourceCell,
isStatic: false);
var origin = new LiveWorldOriginState();
origin.SetPlaceholder(
(int)((SourceLandblock >> 24) & 0xFFu),
(int)((SourceLandblock >> 16) & 0xFFu));
var animatedEntities =
new LiveEntityAnimationRuntimeView(
new LiveEntityRuntimeSlot());
var remotePlacementDrive = new RuntimeRemotePlacementDriveController(
Lifetime,
new GameRuntimeClock(),
new NoopCollisionSource(),
ServiceWindow);
var acceptedPositionDrive = new RuntimeAcceptedPositionDriveController(
Lifetime,
new GameRuntimeClock(),
new NoopCollisionSource(),
new LocalPlayerOutboundController(static (_, _, _, _, _, _) => { }),
static () => new RuntimeGenerationToken(1UL),
static () => 0x50000099u,
static () => null,
static () => false,
static () => null);
var identity = new NoopIdentitySource();
var deletion = new LiveEntityDeletionController(
Runtime,
Lifetime,
new NoopTeardownCoordinator(),
identity);
var hydration = new LiveEntityHydrationController(
Runtime,
Lifetime,
new object(),
new NoopMaterializer(),
new NoopRelationships(),
new NoopReadyPublisher(),
new AlwaysKnownOrigin(),
new NoopNetworkSink(),
new NoopTimestampPublisher(),
identity,
deletion);
var entityEffects = new EntityEffectController(
Runtime,
new AcDream.Core.Vfx.PhysicsScriptRunner(
static _ => null,
new AcDream.Core.Physics.AnimationHookRouter(),
randomUnit: static () => 0.5),
new AcDream.Core.Vfx.PhysicsScriptTableResolver(static _ => null),
new EntityEffectPoseRegistry());
Controller = new LiveEntityNetworkUpdateController(
Runtime,
Lifetime.Objects,
hydration,
entityEffects,
new LiveEntityPresentationController(
Runtime,
Shadows,
(_, _, _) => true,
new LiveEntityPartArrayEnterWorldPort(_ => { })),
new LiveEntityLightController(
Runtime,
new EntityEffectPoseRegistry(),
new AcDream.Core.Lighting.LightingHookSink(
new AcDream.Core.Lighting.LightManager(),
new EntityEffectPoseRegistry()),
static _ => null),
new EquippedChildRenderController(
new NoopDatReaderWriter(),
new object(),
Lifetime.Objects,
Runtime,
new EntityEffectPoseRegistry(),
static _ => false,
static (_, _, _) =>
new ExactProjectionWithdrawalOutcome(
ExactProjectionWithdrawalDisposition.Superseded,
null)),
new ProjectileController(Runtime),
animatedEntities,
new RemoteMovementObservationTracker(),
new RemotePhysicsUpdater(
Lifetime.Physics,
static (_, _) => (0.48f, 1.835f),
static (_, _) => (
System.Collections.Immutable
.ImmutableArray.Empty,
1f, 0.4f, 0.4f),
static (_, _, _, _) => { }),
new RemoteInboundMotionDispatcher(
static (_, _, _) => false,
static (_, _) => { }),
new LiveEntityMotionRuntimeController(
Runtime,
new PhysicsDataCache(),
static () => null,
new AcDream.Core.Selection.SelectionState(),
origin),
engine,
new NoopDatReaderWriter(),
new NoopAnimationLoader(),
combatTargetController: null,
origin,
new NoopTeleportSink(),
_nullClassification
? new NoopLocalPlayerControllerSource()
: new StubLocalPlayerControllerSource(),
new LocalPlayerOutboundController(static (_, _, _, _, _, _) => { }),
new NoopPhysicsHostSource(),
identity,
new FixedScriptTime(),
new NoopSessionSource(),
publishTimestamps: static (_, _) => { },
new NoopMovementTruthSink(),
acceptedPositionDrive,
remotePlacementDrive,
worldDropProjection: null);
}
internal void PublishDestinationCollision()
{
var heights = new byte[81];
Array.Fill(heights, (byte)SpawnHeight);
var heightTable = new float[256];
for (int index = 0; index < heightTable.Length; index++)
heightTable[index] = index;
Lifetime.Physics.ObserveLocalWorldFrame(
SourceCell, teleportAdvanced: false);
Lifetime.Physics.SetPosition.BeginCollisionGeneration(
DestinationLandblock, 1UL);
Lifetime.Physics.Engine.AddLandblock(
DestinationLandblock,
new TerrainSurface(heights, heightTable),
Array.Empty(),
Array.Empty(),
worldOffsetX: DestinationWorldOffset.X,
worldOffsetY: DestinationWorldOffset.Y);
Lifetime.Physics.SetPosition.CommitCollisionGeneration(
DestinationLandblock, 1UL, ready: true);
// The destination landblock must also be a LOADED spatial bucket
// — not merely collision-ready — or IsLiveEntityProjectionResident
// would (correctly) report the entity as not-yet-streamed-in and
// mask the render-layer assertions these tests exist to make.
// Streaming residency itself is out of scope here; C4 route 4b-3
// is about the physics/render sync tail once a destination is
// already resident, matching the two-tier streaming contract.
uint destinationCanonical = CanonicalLandblock(DestinationLandblock);
if (!_spatial.IsLoaded(destinationCanonical))
{
_spatial.AddLandblock(new LoadedLandblock(
destinationCanonical,
new DatReaderWriter.DBObjs.LandBlock(),
Array.Empty()));
}
}
private static uint CanonicalLandblock(uint landblockId) =>
(landblockId & 0xFFFF0000u) | 0xFFFFu;
internal WorldSession.EntityPositionUpdate TeleportUpdate(
Vector3 destination,
uint cellId,
ushort teleportSequence,
uint guid = RemoteGuid,
bool isGrounded = true) => new(
guid,
new CreateObject.ServerPosition(
cellId,
destination.X,
destination.Y,
destination.Z,
1f, 0f, 0f, 0f),
Velocity: null,
PlacementId: null,
IsGrounded: isGrounded,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: teleportSequence,
ForcePositionSequence: 0);
///
/// Item 1(b): installs a real on the
/// remote's canonical incarnation and arms its sticky lease, exactly
/// as the production hydration pipeline does before a live NPC can
/// stick to a target. RunRemoteTeleportHook resolves the host
/// through _liveEntities.TryGetPhysicsHost, so a test that
/// wants the hook's UnStick action to be observable needs the
/// SAME installed host, not merely a bound
/// reader.
///
internal EntityPhysicsHost ArmSticky(uint stickTargetGuid)
{
EntityPhysicsHost host = InstallHost();
host.PositionManager.StickTo(stickTargetGuid, radius: 1f, height: 1f);
Assert.NotEqual(0u, host.PositionManager.GetStickyObjectId());
return host;
}
///
/// A1 fix round (2026-08-04): installs a real
/// WITHOUT arming sticky, so a test
/// can observe whether TryArmConstraintAfterOperation actually
/// armed the leash via host.PositionManager.Constraint — the
/// constraint sub-manager is lazily created only on a real arm, so
/// after a packet is direct proof of zero
/// arming calls.
///
internal EntityPhysicsHost InstallHost()
{
Assert.True(Runtime.TryGetRecord(
_guid, out LiveEntityRecord liveRecord));
var host = new EntityPhysicsHost(
_guid,
getPosition: () => new AcDream.Core.Physics.Position(
Remote.CellId, Remote.Body.Position, Remote.Body.Orientation),
getVelocity: () => Remote.Body.Velocity,
getRadius: () => 0.48f,
inContact: () => Remote.Body.InContact,
minterpMaxSpeed: () => null,
curTime: () => 0d,
physicsTimerTime: () => 0d,
getObjectA: _ => null,
handleUpdateTarget: _ => { },
interruptCurrentMovement: () => { });
Runtime.InstallPhysicsHost(liveRecord, host);
Remote.MarkFullPhysicsHostBound();
return host;
}
internal void DrainPlacementFifo()
{
while (Lifetime.Physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot head))
{
if (!Lifetime.Physics.SetPosition.AcknowledgeProjection(head.Token))
break;
}
}
public void Dispose() => Lifetime.Dispose();
internal sealed class RemoteServiceWindow : IRuntimeRemotePlacementServiceWindow
{
private readonly HashSet _within = [];
internal void Allow(uint landblockId) =>
_within.Add((landblockId & 0xFFFF0000u) | 0xFFFFu);
public bool IsWithinServiceWindow(uint landblockId) =>
_within.Contains((landblockId & 0xFFFF0000u) | 0xFFFFu);
}
private sealed class NoopResources : ILiveEntityResourceLifecycle
{
public void Register(WorldEntity entity) { }
public void Unregister(WorldEntity entity) { }
}
private sealed class NoopCollisionSource : IPreparedCollisionSource
{
public PreparedAssetPresence ProbeCollision(
PakAssetType type, uint sourceFileId) =>
PreparedAssetPresence.Available;
public PreparedCollisionReadResult
ReadSetupCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
PreparedCollisionReadResult.Loaded(
new FlatSetupCollision(
System.Collections.Immutable
.ImmutableArray.Empty,
[new FlatCollisionSphere(Vector3.Zero, 0.48f)],
height: 0f,
radius: 0f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f));
public PreparedCollisionReadResult
ReadGfxObjCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public PreparedCollisionReadResult
ReadCellStructureCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public PreparedCollisionReadResult
ReadEnvCellTopology(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public PreparedCollisionSourceStats CollisionStats => default;
public void Dispose() { }
}
private sealed class NoopIdentitySource : ILocalPlayerIdentitySource
{
public uint ServerGuid => 0x50000099u;
}
private sealed class NoopTeardownCoordinator
: ILiveEntityTeardownCoordinator
{
public void TearDown(LiveEntityRecord record) { }
public void ForgetUnknownOwner(uint serverGuid) { }
}
private sealed class NoopMaterializer : ILiveEntityProjectionMaterializer
{
public bool TryMaterialize(
RuntimeEntityRecord expectedCanonical,
WorldSession.EntitySpawn canonicalSpawn,
LiveProjectionPurpose purpose,
ulong expectedCreateIntegrationVersion,
AcDream.App.Rendering.LiveEntityAppearanceUpdateState?
appearanceUpdate = null) =>
throw new InvalidOperationException(
"The fixture pre-materializes the remote entity; " +
"TryMaterialize should never be reached for an " +
"already-projected accepted Position.");
public void ResetSessionState() { }
}
private sealed class NoopRelationships : ILiveEntityRelationshipProjection
{
public void OnSpawn(WorldSession.EntitySpawn spawn) { }
public void OnParent(ParentEvent.Parsed update) { }
public void OnCreateParentAccepted(CreateParentUpdate update) { }
public AcDream.App.Rendering.ChildUnparentDisposition
OnChildBecameUnparented(uint childGuid) =>
AcDream.App.Rendering.ChildUnparentDisposition.NotAttached;
public bool TryApplyAttachedAppearance(
LiveEntityRecord record, ulong objDescAuthorityVersion) => false;
}
private sealed class NoopReadyPublisher : ILiveEntityReadyPublisher
{
public bool Publish(LiveEntityReadyCandidate candidate) => true;
}
private sealed class AlwaysKnownOrigin : ILiveEntityWorldOriginCoordinator
{
public bool IsKnown => true;
public LiveEntityOriginInitialization TryInitialize(
WorldSession.EntitySpawn spawn) => new(true, []);
}
private sealed class NoopNetworkSink : ILiveEntityNetworkUpdateSink
{
public void ApplySameGeneration(SameGenerationCreateObjectEvents events) { }
}
private sealed class NoopTimestampPublisher
: IAcceptedLocalPhysicsTimestampPublisher
{
public void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps) { }
}
private sealed class NoopDatReaderWriter : IDatReaderWriter
{
private readonly StubDatabase _portal = new();
private readonly StubDatabase _highRes = new();
private readonly StubDatabase _language = new();
private readonly StubDatabase _cell = new();
public string SourceDirectory => string.Empty;
public IDatDatabase Portal => _portal;
public IDatDatabase Cell => _cell;
public ReadOnlyDictionary CellRegions { get; } =
new(new Dictionary());
public IDatDatabase HighRes => _highRes;
public IDatDatabase Language => _language;
public IDatDatabase Local => _language;
public ReadOnlyDictionary RegionFileMap { get; } =
new(new Dictionary());
public int PortalIteration => 0;
public int CellIteration => 0;
public int HighResIteration => 0;
public int LanguageIteration => 0;
public bool TryGetFileBytes(
uint regionId,
uint fileId,
ref byte[] bytes,
out int bytesRead)
{
bytesRead = 0;
return false;
}
public IEnumerable GetAllIdsOfType() where T : IDBObj =>
Array.Empty();
public IEnumerable ResolveId(uint id) =>
Array.Empty();
public bool TrySave(T obj, int iteration = 0) where T : IDBObj =>
throw new NotSupportedException();
public bool TrySave(
uint regionId,
T obj,
int iteration = 0) where T : IDBObj =>
throw new NotSupportedException();
[return: MaybeNull]
public T Get(uint fileId) where T : IDBObj => default;
public bool TryGet(
uint fileId,
[MaybeNullWhen(false)] out T value) where T : IDBObj
{
value = default;
return false;
}
public void Dispose() { }
}
private sealed class StubDatabase : IDatDatabase
{
public DatDatabase Db => throw new NotSupportedException();
public int Iteration => 0;
public IEnumerable GetAllIdsOfType() where T : IDBObj =>
Array.Empty();
public bool TryGet(
uint fileId,
[MaybeNullWhen(false)] out T value) where T : IDBObj
{
value = default;
return false;
}
public bool TryGetFileBytes(
uint fileId,
[MaybeNullWhen(false)] out byte[] value)
{
value = null;
return false;
}
public bool TryGetFileBytes(
uint fileId,
ref byte[] bytes,
out int bytesRead)
{
bytesRead = 0;
return false;
}
public bool TrySave(T obj, int iteration = 0) where T : IDBObj =>
throw new NotSupportedException();
public void Dispose() { }
}
private sealed class NoopAnimationLoader : IAnimationLoader
{
public Animation? LoadAnimation(uint id) => null;
}
private sealed class NoopTeleportSink : ILocalPlayerTeleportNetworkSink
{
public void OnTeleportStarted(uint sequence) { }
public void OfferDestination(
RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced)
{ }
public void ResetSession() { }
public void ResetGenerationPresentation() { }
}
///
/// A makes
/// ClassifyRemoteAcceptedPosition's player_distance
/// input null, which
///
/// then refuses outright (no fabricated distance) — silently
/// starving every remote classification, including the teleport
/// arm this fixture exists to exercise. A real (if origin-parked)
/// controller supplies a finite distance instead.
///
private sealed class StubLocalPlayerControllerSource
: IRuntimeLocalPlayerControllerSource
{
public PlayerMovementController? Controller { get; } =
new PlayerMovementController(new PhysicsEngine());
}
///
/// Fix round (2026-08-04): the deliberate INVERSE of
/// — a null
/// starves player_distance, which
/// makes ClassifyRemoteAcceptedPosition return a null route
/// (the exact "login window" shape D2 exists for), rather than a
/// real classification. Used ONLY by tests that specifically need a
/// null-classified packet.
///
private sealed class NoopLocalPlayerControllerSource
: IRuntimeLocalPlayerControllerSource
{
public PlayerMovementController? Controller => null;
}
private sealed class NoopPhysicsHostSource : ILocalPlayerPhysicsHostSource
{
public EntityPhysicsHost? Host => null;
}
private sealed class FixedScriptTime : IPhysicsScriptTimeSource
{
public double CurrentScriptTime => 1_700_000_000d;
}
private sealed class NoopSessionSource : ILiveWorldSessionSource
{
public WorldSession? CurrentSession => null;
}
private sealed class NoopMovementTruthSink : IMovementTruthDiagnosticSink
{
public void OnOutbound(
string kind,
uint sequence,
MovementResult result,
Vector3 wirePosition,
uint wireCellId,
byte contactByte)
{ }
public void OnServerEcho(
WorldSession.EntityPositionUpdate update,
Vector3 serverWorldPosition)
{ }
public void ResetSession() { }
}
}
}