acdream/tests/AcDream.App.Tests/Physics/LiveEntityNetworkRemoteTeleportPresentationTests.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

1133 lines
50 KiB
C#

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;
/// <summary>
/// C4 route 4b-3, test-plan item 9 (the #312 layer, strong form): drives the
/// COMPLETE production entry point — a real
/// <see cref="LiveEntityNetworkUpdateController.OnPosition"/> call, not the
/// extracted <see cref="LiveEntityNetworkUpdateController.ApplyRemoteContactRouting"/>
/// seam the far-snap/steady-state integration tests use — for a remote
/// teleport, and asserts the render layer directly: the <see cref="WorldEntity"/>
/// pose, <c>ParentCellId</c>, spatial visibility, and collision-shadow
/// publication. #312 shipped because a presentation-restore fix's tests
/// asserted only <c>InWorld</c>/clock/residency, never this layer — these
/// tests exist so the same class of regression cannot ship silently for the
/// teleport arm.
///
/// <para>
/// Every dependency is a REAL production class except the ~15 interface
/// seams <c>LiveEntityNetworkUpdateController</c> takes for local-player-only
/// or DAT-backed concerns that a remote-teleport call never reaches (verified
/// by reading <c>OnPosition</c>: <c>_dats</c>/<c>_animLoader</c> are read only
/// from <c>OnMotion</c>; <c>_localPlayerTeleport</c>/<c>_playerHostSource</c>/
/// <c>_acceptedPositionDrive</c> are gated on <c>update.Guid ==
/// _playerServerGuid</c>, which the remote guid here never satisfies). The
/// canonical <see cref="RuntimeEntityObjectLifetime"/> is shared between
/// <see cref="LiveEntityRuntime"/> and <see cref="RuntimeRemotePlacementDriveController"/>,
/// exactly like production's <c>SessionPlayerComposition</c> wires them.
/// </para>
///
/// <para>
/// <see cref="RemoteGuid"/> deliberately does NOT satisfy
/// <c>IsPlayerGuid</c> (<c>(guid &amp; 0xFF000000u) == 0x50000000u</c>), so
/// <c>OnPosition</c> takes the "GROUNDED ROUTING" creature/NPC branch and
/// its own render+shadow tail (the block after the
/// <c>#184: sync the NPC shadow...</c> comment), not the sibling copy inside
/// the <c>IsPlayerGuid</c> 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.
/// </para>
/// </summary>
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;
/// <summary>
/// In <c>IsPlayerGuid</c>'s <c>0x50xxxxxx</c> range but distinct from
/// the fixture's own <c>NoopIdentitySource.ServerGuid</c>
/// (<c>0x50000099u</c>) — an OTHER player's remote character, exactly
/// the guid shape that takes <c>OnPosition</c>'s <c>IsPlayerGuid</c>
/// branch (and its own airborne "landing block" early return) while
/// still classifying as a remote (<c>update.Guid != _playerServerGuid</c>).
/// </summary>
private const uint OtherPlayerGuid = 0x50006001u;
private const float SpawnHeight = 7f;
/// <summary>
/// Retail's foot-sphere convention (<c>PhysicsEngine.Resolve</c>'s
/// <c>FootSphereCenterLift</c>, #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 <c>terrainHeight + FootSphereCenterLift</c>, not the raw wire Z.
/// </summary>
private const float FootSphereCenterLift = 0.48f;
/// <summary>
/// The strong form of test-plan item 9's commit half: after a remote
/// teleport commits, the render <see cref="WorldEntity"/> pose equals the
/// resolved body pose, <c>ParentCellId</c> 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 <c>Entity.Position</c> at the pre-teleport spawn pose and
/// the shadow registry's position unsynced, both asserted below.
/// </summary>
[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();
}
/// <summary>
/// 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.
///
/// <para>
/// A4 fix round (2026-08-04): every prior assertion here was ALSO
/// satisfied by a regression that misclassified this packet as
/// <c>UnroutedCatchUp</c> instead of the teleport arm — its
/// <c>ApplyInterpolate</c> hard-place lands the body at the exact same
/// wire pose (AP-87's <c>bodyToTarget &gt; 4 m</c> 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:
/// <c>UnroutedCatchUp</c> never reaches <c>ApplyRemoteContactRouting</c>'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 <c>UnStick</c>.
/// </para>
/// </summary>
[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();
}
/// <summary>
/// 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 (<c>UnStick</c> is
/// <c>teleport_hook</c>'s second action, @0x00514EEE). The exact gate
/// under test is
/// <c>LiveEntityNetworkUpdateController.OnPosition</c>'s
/// <c>if (!snapSuppressedByStick || isTeleportRoute)</c> — a stuck NPC's
/// teleport packet must still run the hook (observable here as
/// <c>UnStick</c> actually clearing the sticky lease) and place
/// canonically, exactly as an unstuck NPC's would.
/// </summary>
[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();
}
/// <summary>
/// Test-plan item 6 / contract D5, the OnPosition-level restatement:
/// "the player arm's landing block (<c>!rmState.Body.InContact</c>
/// hard-snap + return) must not claim a teleport-classified packet" and
/// "the 4a <c>IsAirborneNoOperation</c> early returns... cannot claim a
/// <c>SetPosition</c> route". <see cref="OtherPlayerGuid"/> takes
/// <c>OnPosition</c>'s <c>IsPlayerGuid</c> branch (a DIFFERENT code path
/// than every other test in this file, which use the creature-guid
/// branch) — an ungrounded wire packet (<c>IsGrounded: false</c>) for a
/// teleport-classified body must still reach the canonical placement
/// through that branch's own copy of <c>ApplyRemoteContactRouting</c>,
/// not the branch's airborne early return at the top of the
/// <c>IsPlayerGuid</c> block.
/// </summary>
[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();
}
/// <summary>
/// A1 fix round (2026-08-04): the NPC arm must still arm the leash for
/// an ordinary landing packet — wire IS grounded (retail's
/// <c>arg4 != 0</c>), body is NOT in contact (acdream's own
/// snap-vs-interpolate axis, unrelated to retail's arming predicate).
/// <see cref="LiveEntityNetworkUpdateController.ApplyRemoteContactRouting"/>'s
/// free-flight carve-out claims this as <c>AirborneSnap</c> regardless of
/// classification (near/far/leftover would all otherwise apply); before
/// the fix, <c>ToConstraintArm</c> mapped <c>AirborneSnap</c> to the one
/// Runtime value that never arms, so this exact packet — a creature
/// knocked off a ledge — armed ZERO times where retail's
/// <c>MoveOrTeleport</c> returns nonzero and arms unconditionally
/// @0x00454272. <c>host.PositionManager.Constraint</c> 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.
/// </summary>
[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);
}
/// <summary>
/// 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 <c>remotePlacementRequired</c> 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 <c>(worldPos - LastServerPos) / elapsed</c>
/// across the WHOLE teleport distance over one packet interval —
/// hundreds to thousands of m/s — and installing it as
/// <c>ServerVelocity</c>. <see cref="Fixture.Remote"/>'s
/// <c>LastServerPos</c>/<c>LastServerPosTime</c> are seeded to simulate
/// an already-tracked creature (the real <c>@teleto</c>-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.
/// </summary>
[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();
}
/// <summary>
/// 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/
/// <c>Rejected*</c>-classified NPC packet fell through to
/// <see cref="LiveEntityNetworkUpdateController.ApplyRemoteContactRouting"/>'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
/// <c>return 0</c> (@0x0051636D) produces. <see cref="NoopLocalPlayerControllerSource"/>
/// forces the null-classification shape (no local player exists yet to
/// supply <c>player_distance</c> — the login-window case D2 is scoped
/// to); <c>teleportSequence: 1</c> matches the spawn's own committed
/// teleport timestamp so this packet does NOT advance TELEPORT_TS
/// (never teleport-classified, even setting classification aside).
/// </summary>
[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;
/// <param name="guid">
/// Defaults to <see cref="RemoteGuid"/> (an ordinary creature-style
/// guid, NOT in <c>IsPlayerGuid</c>'s <c>0x50xxxxxx</c> range).
/// Tests that need the OTHER production remote branch — the
/// <c>IsPlayerGuid(update.Guid)</c> block in <c>OnPosition</c>, which
/// has its own airborne early return (the "player arm's landing
/// block") — pass a guid in that range instead.
/// </param>
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<CellSurface>(),
Array.Empty<PortalPlane>(),
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<WorldEntity>()));
_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<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
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<LiveEntityAnimationState>(
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<FlatCollisionSphere>.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<CellSurface>(),
Array.Empty<PortalPlane>(),
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<WorldEntity>()));
}
}
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);
/// <summary>
/// Item 1(b): installs a real <see cref="EntityPhysicsHost"/> 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. <c>RunRemoteTeleportHook</c> resolves the host
/// through <c>_liveEntities.TryGetPhysicsHost</c>, so a test that
/// wants the hook's <c>UnStick</c> action to be observable needs the
/// SAME installed host, not merely a bound <see cref="RemoteMotion.Host"/>
/// reader.
/// </summary>
internal EntityPhysicsHost ArmSticky(uint stickTargetGuid)
{
EntityPhysicsHost host = InstallHost();
host.PositionManager.StickTo(stickTargetGuid, radius: 1f, height: 1f);
Assert.NotEqual(0u, host.PositionManager.GetStickyObjectId());
return host;
}
/// <summary>
/// A1 fix round (2026-08-04): installs a real
/// <see cref="EntityPhysicsHost"/> WITHOUT arming sticky, so a test
/// can observe whether <c>TryArmConstraintAfterOperation</c> actually
/// armed the leash via <c>host.PositionManager.Constraint</c> — the
/// constraint sub-manager is lazily created only on a real arm, so
/// <see langword="null"/> after a packet is direct proof of zero
/// arming calls.
/// </summary>
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<uint> _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<FlatSetupCollision>
ReadSetupCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
PreparedCollisionReadResult<FlatSetupCollision>.Loaded(
new FlatSetupCollision(
System.Collections.Immutable
.ImmutableArray<FlatCollisionCylinder>.Empty,
[new FlatCollisionSphere(Vector3.Zero, 0.48f)],
height: 0f,
radius: 0f,
stepUpHeight: 0.4f,
stepDownHeight: 0.4f));
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset>
ReadGfxObjCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
ReadCellStructureCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public PreparedCollisionReadResult<FlatEnvCellTopology>
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<uint, IDatDatabase> CellRegions { get; } =
new(new Dictionary<uint, IDatDatabase>());
public IDatDatabase HighRes => _highRes;
public IDatDatabase Language => _language;
public IDatDatabase Local => _language;
public ReadOnlyDictionary<uint, uint> RegionFileMap { get; } =
new(new Dictionary<uint, uint>());
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<uint> GetAllIdsOfType<T>() where T : IDBObj =>
Array.Empty<uint>();
public IEnumerable<IDatReaderWriter.IdResolution> ResolveId(uint id) =>
Array.Empty<IDatReaderWriter.IdResolution>();
public bool TrySave<T>(T obj, int iteration = 0) where T : IDBObj =>
throw new NotSupportedException();
public bool TrySave<T>(
uint regionId,
T obj,
int iteration = 0) where T : IDBObj =>
throw new NotSupportedException();
[return: MaybeNull]
public T Get<T>(uint fileId) where T : IDBObj => default;
public bool TryGet<T>(
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<uint> GetAllIdsOfType<T>() where T : IDBObj =>
Array.Empty<uint>();
public bool TryGet<T>(
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>(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() { }
}
/// <summary>
/// A <see langword="null"/> <see cref="Controller"/> makes
/// <c>ClassifyRemoteAcceptedPosition</c>'s <c>player_distance</c>
/// input null, which
/// <see cref="AcDream.Runtime.Physics.RuntimeAcceptedPositionRouteRequests.TryBuild"/>
/// 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.
/// </summary>
private sealed class StubLocalPlayerControllerSource
: IRuntimeLocalPlayerControllerSource
{
public PlayerMovementController? Controller { get; } =
new PlayerMovementController(new PhysicsEngine());
}
/// <summary>
/// Fix round (2026-08-04): the deliberate INVERSE of
/// <see cref="StubLocalPlayerControllerSource"/> — a null
/// <see cref="Controller"/> starves <c>player_distance</c>, which
/// makes <c>ClassifyRemoteAcceptedPosition</c> 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.
/// </summary>
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() { }
}
}
}