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;
using DatReaderWriter.Types;
// Alias the DatReaderWriter enum so it doesn't clash with
// AcDream.Core.Physics.MotionCommand (a static class of uint constants).
using DRWMotionCommand = DatReaderWriter.Enums.MotionCommand;
namespace AcDream.App.Tests.Physics;
///
/// The OnPosition collapse's referee (contract §5,
/// docs/research/2026-08-04-onposition-collapse-contract.md): every scenario
/// below is driven for BOTH a 0x50xxxxxx player-range guid and a
/// 0x8xxxxxxx creature-range guid through the SAME production
/// entry point —
/// never the extracted ApplyRemoteContactRouting seam directly. Before
/// the collapse this class did not exist as a structural pattern: every
/// existing test in this file's siblings drove exactly one guid range, which
/// is precisely how the 4b-3 defects (A1's zero-arm leash regression, A2/R3's
/// synthesized-velocity defect) survived — a defect in one hand-written copy
/// left the other copy's tests green. After the collapse there is one code
/// path, so running both guids through it is cheap; the point is it STAYS a
/// pair so a future re-divergence (a guid-gated edit smuggled into the
/// unified tail) fails a test instead of hiding.
///
///
/// This class reuses the
/// fixture's exact composition-only construction pattern (the controller's
/// 67+ collaborators are wired only by SessionPlayerComposition in
/// production, so a source pin would be the alternative — this drives the
/// real class instead).
///
///
public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
{
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);
/// Player-range guid — 0x50xxxxxx, distinct from the
/// fixture's own local-player NoopIdentitySource.ServerGuid
/// (0x50000099u).
private const uint PlayerGuid = 0x50007001u;
/// Creature-range guid — 0x8xxxxxxx, matching the
/// connected-gate evidence in the 4b-3 contract for how real creature
/// guids arrive on the wire.
private const uint CreatureGuid = 0x80007001u;
private const float SpawnHeight = 7f;
private const float FootSphereCenterLift = 0.48f;
private static bool IsPlayer(uint guid) => guid == PlayerGuid;
// ── Scenario 0: C5b's untested load-bearing claim ───────────────────
///
/// C5b (#275) review, findings L1/L2. C5b made the steady-state merge stop
/// stamping the wire cell and justified it with a claim it never tested:
/// "production installs the bucket at W2 in the same call (verified: no
/// return between the recovery call and W2 is conditioned on
/// IsSpatiallyProjected or FullCellId)". The commit also asserted "no
/// fixture covers pickup at that layer" — this file drives the real
/// at ~26 call
/// sites, and every W2 assertion the commit added hand-calls
/// RebucketLiveEntity/CommitRebucket itself, so nothing
/// drove the sequence end-to-end. This does.
///
/// The scenario is the leave-world/re-entry edge the production
/// comment at LiveEntityNetworkUpdateController.cs:2073-2077 names:
/// the record keeps WorldEntity as its logical/render-resource
/// owner while IsSpatiallyProjected is false, and "a fresh retail
/// Position is the re-entry edge; testing only for the retained object
/// reference leaves dropped inventory permanently invisible after
/// InventoryPutObjectIn3D". The materializer DECLINES (the post-C5b
/// self-projection state — DatLiveEntityProjectionMaterializer.cs:767
/// now sees a canonical cell the merge did not advance), so the only
/// things that can restore the bucket and the cell are the two wire-cell
/// channels AD-60 says deliberately survive C5b.
///
/// Measured at the review, and NOT what C5b's commit message
/// assumed: W2 and W3 are REDUNDANT here. Sabotaging W2 alone (both
/// "adopt the committed cell instead of the wire cell" and "skip the
/// rebucket entirely") leaves this test — and every other test in this
/// file — GREEN, because the post-routing wire-cell adopt
/// (TryAdoptWireCellAfterRouting, W3/AP-135) writes
/// RemoteMotion.CellId, which reads through to canonical
/// FullCellId via CommitCanonicalCell, whose graphical
/// CellCommitted recovery re-installs the bucket. Only removing
/// BOTH surviving channels turns this red — and it is then the ONLY red
/// in the file. So what this test pins is AD-60's actual surviving claim
/// ("two wire-cell writers deliberately remain downstream of the merge"),
/// not W2 in isolation. Naming it after W2 would have been a third
/// contract asserting a mechanism that is not the one doing the work.
///
///
/// Both guid classes per this file's own discipline.
///
[Theory]
[InlineData(PlayerGuid)]
[InlineData(CreatureGuid)]
public void WithdrawnProjection_AcceptedPositionRestoresBucketAndWireCell(
uint guid)
{
using var fixture = new Fixture(guid, decliningMaterializer: true);
Assert.True(fixture.Runtime.TryGetRecord(guid, out LiveEntityRecord record));
Assert.True(record.IsSpatiallyProjected);
Assert.Equal(SourceCell, record.FullCellId);
// The withdrawal: render bucket gone, logical record + WorldEntity
// retained. This is what makes RequiresSpatialProjectionRecovery true
// on the next accepted Position.
Assert.True(fixture.Runtime.WithdrawLiveEntityProjection(guid));
Assert.False(record.IsSpatiallyProjected);
Assert.NotNull(record.WorldEntity);
Assert.Equal(SourceCell, record.FullCellId);
const uint WireCell = SourceLandblock | 0x0002u;
fixture.Controller.OnPosition(fixture.Update(
new Vector3(13f, 15f, SpawnHeight),
WireCell,
teleportSequence: 1,
guid: guid));
// The recovery branch really ran and really declined — without this
// the assertions below could pass on an entity that never needed
// recovering at all.
Assert.True(fixture.Lifetime.Entities.TryGetActive(
guid, out RuntimeEntityRecord canonical));
// The packet really was accepted and really did reach the recovery
// branch, which really did decline — without these the cell assertions
// below could pass on a rejected packet, or on an entity that never
// needed recovering at all.
Assert.Equal(
WireCell,
canonical.Snapshot.Position!.Value.LandblockId);
Assert.True(fixture.MaterializerDeclined);
// W2 is what closes both halves.
Assert.True(record.IsSpatiallyProjected);
Assert.Equal(WireCell, record.FullCellId);
Assert.Equal(WireCell, canonical.FullCellId);
Assert.Equal(WireCell, fixture.Entity.ParentCellId);
}
// ── Scenario 1: teleport commit ─────────────────────────────────────
[Theory]
[InlineData(PlayerGuid)]
[InlineData(CreatureGuid)]
public void TeleportCommit_BothGuids_ArmsOnceAndNeverInstallsVelocity(uint guid)
{
using var fixture = new Fixture(guid);
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
EntityPhysicsHost host = fixture.InstallHost();
Assert.Null(host.PositionManager.Constraint);
var destination = new Vector3(12f, 14f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
destination, DestinationCell, teleportSequence: 5, guid: guid));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
guid, 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(body.Position, fixture.Entity.Position);
Assert.Equal(DestinationCell, fixture.Entity.ParentCellId);
Assert.Equal(DestinationCell, canonical.FullCellId);
// The leash armed exactly once (D4: teleport arms on every outcome).
Assert.NotNull(host.PositionManager.Constraint);
// Row 6/invariant-6 (contract §5 scenario 1): retail's teleport
// branch writes NO velocity at all, for either guid.
Assert.False(fixture.Remote.HasServerVelocity);
Assert.Equal(Vector3.Zero, fixture.Remote.ServerVelocity);
// The shadow published at the resolved position.
ShadowEntry shadowEntry = Assert.Single(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == fixture.Entity.Id);
Assert.Equal(body.Position, shadowEntry.Position);
fixture.DrainPlacementFifo();
}
// ── Scenario 2: landing packet (the preserved rows 2a/2b asymmetry) ─
[Fact]
public void LandingPacket_PlayerGuid_QueueClearedNoShadowPublish_316Preserved()
{
using var fixture = new Fixture(PlayerGuid);
EntityPhysicsHost host = fixture.InstallHost();
Assert.Null(host.PositionManager.Constraint);
// Mid-arc: body not in contact, wire IS grounded — the LANDING
// scenario. Pre-populate the interp queue so the clear is observable.
fixture.Remote.Body.TransientState = TransientStateFlags.Active;
fixture.Remote.Interp.Enqueue(
new Vector3(1f, 1f, SpawnHeight),
Quaternion.Identity,
isMovingTo: false,
currentBodyPosition: new Vector3(50f, 50f, SpawnHeight),
currentBodyOrientation: Quaternion.Identity);
Assert.True(fixture.Remote.Interp.IsActive);
Vector3 spawnShadowPos = Assert.Single(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == fixture.Entity.Id).Position;
var landingPos = new Vector3(12f, 14f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
landingPos, SourceCell, teleportSequence: 1,
guid: PlayerGuid, isGrounded: true));
// Body snapped to the landing pose; entity synced from the resolved
// body; armed exactly once.
Assert.Equal(landingPos, fixture.Remote.Body.Position);
Assert.Equal(landingPos, fixture.Entity.Position);
Assert.Equal(SourceCell, fixture.Entity.ParentCellId);
Assert.NotNull(host.PositionManager.Constraint);
// Row 2a, PRESERVED for player guids: the interp queue IS cleared.
Assert.False(fixture.Remote.Interp.IsActive);
// Row 2b / #316, PRESERVED (NOT fixed): the shadow is NOT
// republished for a player-guid landing — it stays at whatever it
// was before this packet.
ShadowEntry shadowEntry = Assert.Single(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == fixture.Entity.Id);
Assert.Equal(spawnShadowPos, shadowEntry.Position);
Assert.NotEqual(landingPos, shadowEntry.Position);
// AP-135's cell-adopt bookkeeping still ran (via the post-routing
// wire-cell adopt, not suppressed for AirborneSnap).
Assert.Equal(SourceCell, fixture.Remote.CellId);
}
[Fact]
public void LandingPacket_CreatureGuid_ShadowPublishedQueueNotCleared()
{
using var fixture = new Fixture(CreatureGuid);
EntityPhysicsHost host = fixture.InstallHost();
Assert.Null(host.PositionManager.Constraint);
fixture.Remote.Body.TransientState = TransientStateFlags.Active;
fixture.Remote.Interp.Enqueue(
new Vector3(1f, 1f, SpawnHeight),
Quaternion.Identity,
isMovingTo: false,
currentBodyPosition: new Vector3(50f, 50f, SpawnHeight),
currentBodyOrientation: Quaternion.Identity);
Assert.True(fixture.Remote.Interp.IsActive);
var landingPos = new Vector3(12f, 14f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
landingPos, SourceCell, teleportSequence: 1,
guid: CreatureGuid, isGrounded: true));
Assert.Equal(landingPos, fixture.Remote.Body.Position);
Assert.Equal(landingPos, fixture.Entity.Position);
Assert.NotNull(host.PositionManager.Constraint);
// Row 2a, PRESERVED for creature guids: the interp queue is NOT
// cleared here (the per-tick AP-139 edge owns it — see the arm's
// comment in OnPosition).
Assert.True(fixture.Remote.Interp.IsActive);
// Row 2b, the NPC/creature half: the shadow DOES publish at the
// resolved body pose — this is the behaviour #316 says the player
// half is missing.
ShadowEntry shadowEntry = Assert.Single(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == fixture.Entity.Id);
Assert.Equal(landingPos, shadowEntry.Position);
}
// ── Scenario 3: wire-airborne, null-classified (login-window shape) ─
[Theory]
[InlineData(PlayerGuid)]
[InlineData(CreatureGuid)]
public void WireAirborneNullClassified_BothGuids_WritesOnlyAP135Bookkeeping(
uint guid)
{
using var fixture = new Fixture(guid, 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.Update(
wirePos, SourceCell, teleportSequence: 1,
guid: guid, isGrounded: false));
// No body write.
Assert.Equal(spawnBodyPose, fixture.Remote.Body.Position);
// No shadow republish.
ShadowEntry shadowEntry = Assert.Single(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == fixture.Entity.Id);
Assert.Equal(spawnBodyPose, shadowEntry.Position);
// No arm (round-2 architecture review's discriminator: Constraint is
// lazily created only on a genuine arm).
Assert.Null(host.PositionManager.Constraint);
// Positive half (round-2 B1's lesson): AP-135's two writes DID
// happen.
Assert.Equal(SourceCell, fixture.Remote.CellId);
Assert.Equal(wirePos, fixture.Remote.LastServerPos);
Assert.NotEqual(0d, fixture.Remote.LastServerPosTime);
}
// ── Scenario 4: airborne no-op (NoPositionOperation, non-null route) ─
[Theory]
[InlineData(PlayerGuid)]
[InlineData(CreatureGuid)]
public void AirborneNoOperation_BothGuids_WritesOnlyAP135BookkeepingNoArm(
uint guid)
{
using var fixture = new Fixture(guid);
EntityPhysicsHost host = fixture.InstallHost();
Assert.Null(host.PositionManager.Constraint);
// Contact SET so this is NOT the landing/AirborneSnap scenario —
// this packet must classify NoPositionOperation via the WIRE
// has_contact bit (update.IsGrounded), the classifier's OWN
// predicate, independent of the body's own contact state.
fixture.Remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
Vector3 spawnBodyPose = fixture.Remote.Body.Position;
var wirePos = new Vector3(50f, 50f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
wirePos, SourceCell, teleportSequence: 1,
guid: guid, isGrounded: false));
Assert.Equal(spawnBodyPose, fixture.Remote.Body.Position);
ShadowEntry shadowEntry = Assert.Single(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == fixture.Entity.Id);
Assert.Equal(spawnBodyPose, shadowEntry.Position);
Assert.Null(host.PositionManager.Constraint);
Assert.Equal(SourceCell, fixture.Remote.CellId);
Assert.Equal(wirePos, fixture.Remote.LastServerPos);
Assert.NotEqual(0d, fixture.Remote.LastServerPosTime);
}
// ── Scenario 5: near interpolate ─────────────────────────────────────
[Theory]
[InlineData(PlayerGuid)]
[InlineData(CreatureGuid)]
public void NearInterpolate_BothGuids_EnqueuesAndArmsOnce(uint guid)
{
using var fixture = new Fixture(guid);
EntityPhysicsHost host = fixture.InstallHost();
Assert.Null(host.PositionManager.Constraint);
fixture.Remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
// AP-87 backstop: not firstUp, and further than the 4 m snap
// threshold so this packet enqueues rather than snaps.
fixture.Remote.Body.Position = new Vector3(2f, 2f, SpawnHeight);
fixture.Remote.LastServerPos = fixture.Remote.Body.Position;
fixture.Remote.LastServerPosTime =
(DateTime.UtcNow - DateTime.UnixEpoch).TotalSeconds - 0.15;
// Within AP-87's 4 m snap threshold (distance ~2.24 m) but well
// outside InterpolationManager.Enqueue's 0.05 m "already-close" wipe
// — a genuine enqueue, not a snap and not a no-op.
var target = new Vector3(4f, 3f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
target, SourceCell, teleportSequence: 1,
guid: guid, isGrounded: true));
// Enqueued, not snapped: the body pose is unchanged (the per-tick
// catch-up walks toward the queued target — this packet does not
// move the body directly).
Assert.Equal(new Vector3(2f, 2f, SpawnHeight), fixture.Remote.Body.Position);
Assert.True(fixture.Remote.Interp.IsActive);
Assert.NotNull(host.PositionManager.Constraint);
// Row 3: the wire cell still adopts (near-interpolate is not a
// placement arm, so TryAdoptWireCellAfterRouting does not suppress).
Assert.Equal(SourceCell, fixture.Remote.CellId);
// The render entity tracks the (unchanged) body, not the wire pose —
// route 4a's generic-write suppression plus the unified tail's
// resync from the resolved body.
Assert.Equal(fixture.Remote.Body.Position, fixture.Entity.Position);
}
// ── Scenario 6: far snap ─────────────────────────────────────────────
[Theory]
[InlineData(PlayerGuid)]
[InlineData(CreatureGuid)]
public void FarSnap_BothGuids_PlacesAndArmsOnEveryOutcome(uint guid)
{
using var fixture = new Fixture(guid);
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
EntityPhysicsHost host = fixture.InstallHost();
Assert.Null(host.PositionManager.Constraint);
fixture.Remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
Vector3 spawnPose = fixture.Entity.Position;
// Far enough (>=96 m from the stub player controller's origin) via a
// packet whose destination cell differs, WITHOUT advancing
// TELEPORT_TS — the SetPositionSimple (far) classification, not
// SetPosition (teleport/cell-less).
var destination = new Vector3(12f, 14f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
destination, DestinationCell, teleportSequence: 1,
guid: guid, isGrounded: true));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
guid, out RuntimeEntityRecord canonical));
Assert.NotNull(canonical.PhysicsBody);
PhysicsBody body = canonical.PhysicsBody!;
Assert.NotEqual(spawnPose, body.Position);
Assert.Equal(body.Position, fixture.Entity.Position);
Assert.Equal(DestinationCell, fixture.Entity.ParentCellId);
Assert.Equal(DestinationCell, canonical.FullCellId);
Assert.NotNull(host.PositionManager.Constraint);
ShadowEntry shadowEntry = Assert.Single(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == fixture.Entity.Id);
Assert.Equal(body.Position, shadowEntry.Position);
fixture.DrainPlacementFifo();
}
// ── Scenario 7: sticky-suppressed steady-state (row 8's asymmetry) ──
[Fact]
public void StickySuppressed_CreatureGuid_RoutingSkippedButStillArmed()
{
using var fixture = new Fixture(CreatureGuid);
EntityPhysicsHost host = fixture.ArmSticky(stickTargetGuid: 0x70009999u);
fixture.Remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
Vector3 bodyBefore = fixture.Remote.Body.Position;
var target = new Vector3(12f, 14f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
target, SourceCell, teleportSequence: 1,
guid: CreatureGuid, isGrounded: true));
// Routing never ran: body and queue are untouched.
Assert.Equal(bodyBefore, fixture.Remote.Body.Position);
Assert.False(fixture.Remote.Interp.IsActive);
// D4: sticky-suppressed still arms, with UnroutedCatchUp.
Assert.NotNull(host.PositionManager.Constraint);
// Still stuck — nothing in this path unsticks it (only the teleport
// hook does).
Assert.NotEqual(0u, host.PositionManager.GetStickyObjectId());
}
[Fact]
public void StickySuppressed_PlayerGuid_GateNeverAppliesRoutingRunsAnyway()
{
using var fixture = new Fixture(PlayerGuid);
EntityPhysicsHost host = fixture.ArmSticky(stickTargetGuid: 0x70009999u);
fixture.Remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
fixture.Remote.Body.Position = new Vector3(2f, 2f, SpawnHeight);
fixture.Remote.LastServerPos = fixture.Remote.Body.Position;
fixture.Remote.LastServerPosTime =
(DateTime.UtcNow - DateTime.UnixEpoch).TotalSeconds - 0.15;
var target = new Vector3(4f, 3f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
target, SourceCell, teleportSequence: 1,
guid: PlayerGuid, isGrounded: true));
// Row 8's named asymmetry: TS-44 never gates a player guid, so
// routing ran despite sticky being armed — the interp queue is now
// populated (or the body snapped), unlike the creature case above.
Assert.True(fixture.Remote.Interp.IsActive);
Assert.NotNull(host.PositionManager.Constraint);
// Sticky itself is untouched by this non-teleport path (only the
// teleport hook unsticks).
Assert.NotEqual(0u, host.PositionManager.GetStickyObjectId());
}
// ── Scenario 8: NPC velocity-cycle (row 7/14's data-driven survivor) ─
[Fact]
public void VelocityCycle_CreatureGuid_PlansACycleFromWireVelocity()
{
using var fixture = new Fixture(CreatureGuid, withAnimation: true);
EntityPhysicsHost host = fixture.InstallHost();
fixture.Remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
// InitializeState() (fixture ctor) already seeded Ready via the
// motion table's StyleDefaults — verify the precondition rather
// than re-stating the seed.
Assert.Equal(
AcDream.Core.Physics.MotionCommand.Ready,
fixture.Animated!.Sequencer!.CurrentMotion);
var target = new Vector3(12f, 14f, SpawnHeight);
var wireVelocity = new Vector3(0.7f, 0f, 0f);
fixture.Controller.OnPosition(fixture.Update(
target, SourceCell, teleportSequence: 1,
guid: CreatureGuid, isGrounded: true, velocity: wireVelocity));
Assert.True(fixture.Remote.HasServerVelocity);
Assert.Equal(wireVelocity, fixture.Remote.ServerVelocity);
Assert.NotEqual(
AcDream.Core.Physics.MotionCommand.Ready,
fixture.Animated.Sequencer.CurrentMotion);
_ = host;
}
[Fact]
public void VelocityCycle_PlayerGuid_SequencerUntouchedByWireVelocity()
{
using var fixture = new Fixture(PlayerGuid, withAnimation: true);
EntityPhysicsHost host = fixture.InstallHost();
fixture.Remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
Assert.Equal(
AcDream.Core.Physics.MotionCommand.Ready,
fixture.Animated!.Sequencer!.CurrentMotion);
var target = new Vector3(12f, 14f, SpawnHeight);
var wireVelocity = new Vector3(0.7f, 0f, 0f);
fixture.Controller.OnPosition(fixture.Update(
target, SourceCell, teleportSequence: 1,
guid: PlayerGuid, isGrounded: true, velocity: wireVelocity));
// Row 6: the synth-velocity write is still write-only for players —
// it happens (the unified NPC formula runs for every guid), but
// nothing production reads it here either.
Assert.True(fixture.Remote.HasServerVelocity);
// Row 7/14: RemoteServerControlledVelocityCycle.Apply's internal
// IsPlayerGuid return means the sequencer NEVER changes for a player
// guid, regardless of the wire velocity supplied.
Assert.Equal(
AcDream.Core.Physics.MotionCommand.Ready,
fixture.Animated.Sequencer.CurrentMotion);
_ = host;
}
// ── Sabotage check (contract §5, one-time, manual) ──────────────────
//
// Performed by hand during implementation, not committed as a test (a
// committed sabotage would defeat its own purpose): temporarily deleted
// the `RuntimeRemoteSteadyStatePosition.TryArmConstraintAfterOperation`
// call from the unified tail in `OnPosition` and re-ran this file.
// Result: TeleportCommit_BothGuids (both InlineData rows),
// LandingPacket_PlayerGuid, LandingPacket_CreatureGuid,
// NearInterpolate_BothGuids (both rows), FarSnap_BothGuids (both rows),
// and StickySuppressed_CreatureGuid / StickySuppressed_PlayerGuid all
// failed — 9 of 9 non-airborne scenarios, both guid halves of every
// dual-guid Theory. This is the structural fix for the 4b-3 defect
// class: a sabotage that used to leave one guid's tests green now fails
// both, because there is one call site left to sabotage. Reverted before
// committing.
// ── C4 route 5 (A5 fix): missile matrix ─────────────────────────────
//
// The architecture review's FAIL-level finding: zero App tests drove
// OnPosition with a missile packet, so A1 (App discards the seam's
// status) and A2 (an unbound missile's Position silently dropped) both
// lived in the ~35 lines of "thin glue" the implementer argued were
// covered by two well-tested layers. These tests extend THIS file's own
// fixture — never a lighter one — with a genuine Missile-flagged,
// RuntimeProjectile-bound, RemoteMotion-less incarnation, and assert the
// SAME observable surface the remote scenarios above assert: body/entity
// position and ParentCellId (that pair is the exact A1 assertion), plus
// the projectile-specific half — no RemoteMotion is EVER created
// (invariant 8's mutual exclusion) and no early wire-pose write occurs.
// Destinations for the commit scenarios are airborne (well above
// PublishDestinationCollision's flat terrain), isolating the placement
// assertions from the shared pipeline's ordinary ground-contact response
// (Claim 4's confound, restated here for the App layer).
private const uint MissileGuid = 0x80007101u;
private static readonly Vector3 MissileAirborneDestination =
new(12f, 14f, SpawnHeight + 10f);
[Fact]
public void MissileTeleportCommit_PlacesBodyNoRemoteMotionParentCellIdAgreesWithBody()
{
using var fixture = new Fixture(MissileGuid, isMissile: true);
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
fixture.Controller.OnPosition(fixture.Update(
MissileAirborneDestination, DestinationCell, teleportSequence: 5,
guid: MissileGuid, isGrounded: true));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
Assert.Null(canonical.RemoteMotion);
Assert.NotNull(canonical.Projectile);
PhysicsBody body = canonical.PhysicsBody!;
Vector3 resolved = MissileAirborneDestination + DestinationWorldOffset;
Assert.Equal(resolved, body.Position);
Assert.Equal(body.Position, fixture.Entity.Position);
// A1's own assertion: ParentCellId agrees with the RESOLVED body's
// OWN cell — the review's concrete wrong-cell scenario, checked
// positively here rather than only on the no-op scenarios below.
Assert.Equal(body.CellPosition.ObjCellId, fixture.Entity.ParentCellId);
Assert.Equal(DestinationCell, fixture.Entity.ParentCellId);
// D-P4: the collision table was force-ended (no seeded owner here to
// assert a 1->0 transition — that half is covered at
// tests/AcDream.Runtime.Tests — but the call must not throw or
// leave the entity uncollidable long-term; Tick below proves it is
// still a live, ordinary object).
ShadowEntry shadowEntry = Assert.Single(
fixture.Shadows.AllEntriesForDebug(),
entry => entry.EntityId == fixture.Entity.Id);
Assert.Equal(body.Position, shadowEntry.Position);
fixture.DrainPlacementFifo();
}
[Fact]
public void MissileFarCommit_PlacesBodyNoRemoteMotionParentCellIdAgreesWithBody()
{
using var fixture = new Fixture(MissileGuid, isMissile: true);
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
fixture.Controller.OnPosition(fixture.Update(
MissileAirborneDestination, DestinationCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: true));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
Assert.Null(canonical.RemoteMotion);
PhysicsBody body = canonical.PhysicsBody!;
Vector3 resolved = MissileAirborneDestination + DestinationWorldOffset;
Assert.Equal(resolved, body.Position);
Assert.Equal(body.Position, fixture.Entity.Position);
Assert.Equal(body.CellPosition.ObjCellId, fixture.Entity.ParentCellId);
Assert.Equal(DestinationCell, fixture.Entity.ParentCellId);
fixture.DrainPlacementFifo();
}
///
/// B1 EnvCell id — an indoor-format low word (0x100+, outside
/// LandDefs.AdjustToOutside's 1-0x40 outdoor range) staged onto
/// the body BEFORE the store-path dispatch below. This is what makes
/// the regression assertion actually discriminate: for an OUTDOOR
/// source/destination pair, PhysicsBody.Position's setter
/// delta-syncs CellPosition through AdjustToOutside and
/// happens to re-derive the correct destination landblock anyway (pure
/// geometry, no collision data needed) — so a wrong
/// body.CellPosition.ObjCellId read would coincidentally agree
/// with record.FullCellId and the test would pass either way.
/// An INDOOR source cell takes SyncCellPositionDelta's OTHER
/// branch (PhysicsBody.cs:295-300): it carries the position delta
/// but never re-derives the cell id, so body.CellPosition.ObjCellId
/// stays PINNED at the stale indoor source cell through the store path.
/// record.FullCellId has no such blind spot — it is merged from
/// the wire unconditionally — so this is the scenario where the two
/// expressions genuinely diverge and MAJOR 1's revert is provable.
///
private const uint IndoorSourceCell = SourceLandblock | 0x0100u;
///
/// Residual 1 close (round-2 review): the STORE path — Refused —
/// at the App layer, closing both the coverage gap AND standing as the
/// regression test for MAJOR 1 (the ParentCellId revert). The
/// destination is deliberately left outside the service window
/// ( is never called for
/// ), so
/// CanAttemptDestination refuses before the engine ever runs and
/// StoreAcceptedDestinationPose resolves the destination through
/// Runtime's own world frame instead of a commit. Refused is
/// still a storing (A1-admitted) outcome, so the App-level presentation
/// sync runs — the entity's position moves to the DESTINATION and its
/// ParentCellId tracks record.FullCellId, never the stale
/// INDOOR source cell a body.CellPosition.ObjCellId read would
/// have produced (see 's doc comment for
/// why an outdoor source cell would not have discriminated here).
///
///
/// C5b (#275), trap T6: record.FullCellId is now the COMMITTED
/// cell, so a REFUSED placement leaves it at the source. The missile arm
/// returns before the OnPosition generic tail, so its residency is
/// placement-receipt-driven and nothing else — which is retail
/// (a projectile's cell comes from SetPosition, full stop). The
/// MAJOR 1 regression check is unchanged in substance and is asserted
/// directly below as the identity it always meant:
/// ParentCellId == record.FullCellId != body.CellPosition.ObjCellId.
///
///
[Fact]
public void MissileFarRefused_StorePathStillMovesEntityToDestinationParentCellIdAgreesWithCommittedCell()
{
using var fixture = new Fixture(MissileGuid, isMissile: true);
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonicalBeforeStage));
PhysicsBody stagedBody = canonicalBeforeStage.PhysicsBody!;
stagedBody.SnapToCell(
IndoorSourceCell, stagedBody.Position, stagedBody.Position);
Assert.Equal(IndoorSourceCell, stagedBody.CellPosition.ObjCellId);
fixture.PublishDestinationCollision();
// Deliberately NOT allowed — CanAttemptDestination refuses.
fixture.Controller.OnPosition(fixture.Update(
MissileAirborneDestination, DestinationCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: true));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
Assert.Null(canonical.RemoteMotion);
Assert.NotNull(canonical.Projectile);
PhysicsBody body = canonical.PhysicsBody!;
Vector3 resolved = MissileAirborneDestination + DestinationWorldOffset;
Assert.Equal(resolved, body.Position);
Assert.True(body.InWorld);
// The store fallback never re-derives an INDOOR cell id — confirms
// the divergence this test is built to exercise actually occurred.
Assert.Equal(IndoorSourceCell, body.CellPosition.ObjCellId);
// The exact B1 regression check: ParentCellId IS record.FullCellId —
// never body.CellPosition.ObjCellId, which just asserted it is STILL
// the stale indoor source cell.
Assert.Equal(body.Position, fixture.Entity.Position);
Assert.Equal(canonical.FullCellId, fixture.Entity.ParentCellId);
Assert.NotEqual(
body.CellPosition.ObjCellId,
fixture.Entity.ParentCellId);
// C5b: the refused placement committed nothing, so the committed cell
// is still the source. The wire cell is NOT residency here.
Assert.Equal(SourceCell, canonical.FullCellId);
Assert.NotEqual(DestinationCell, fixture.Entity.ParentCellId);
fixture.DrainPlacementFifo();
}
[Fact]
public void MissileNear_NoOp_BodyUnchangedNoRemoteMotionNoWirePoseWrite()
{
using var fixture = new Fixture(MissileGuid, isMissile: true);
Vector3 spawnPose = fixture.Entity.Position;
var target = new Vector3(4f, 3f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
target, SourceCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: true));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
Assert.Null(canonical.RemoteMotion);
PhysicsBody body = canonical.PhysicsBody!;
Assert.Equal(spawnPose, body.Position);
// A1's regression, asserted directly: no early wire-pose write —
// the render entity was never moved to `target`.
Assert.Equal(spawnPose, fixture.Entity.Position);
Assert.NotEqual(target, fixture.Entity.Position);
}
[Fact]
public void MissileAirborne_NoOp_BodyUnchangedNoRemoteMotion()
{
using var fixture = new Fixture(MissileGuid, isMissile: true);
Vector3 spawnPose = fixture.Entity.Position;
var wirePos = new Vector3(50f, 50f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
wirePos, SourceCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: false));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
Assert.Null(canonical.RemoteMotion);
PhysicsBody body = canonical.PhysicsBody!;
Assert.Equal(spawnPose, body.Position);
Assert.Equal(spawnPose, fixture.Entity.Position);
}
[Fact]
public void MissileNullClassification_Swallowed_NoRemoteMotionNoWriteNoPredictionChange()
{
using var fixture = new Fixture(
MissileGuid, nullClassification: true, isMissile: true);
Vector3 spawnPose = fixture.Entity.Position;
ulong predictionBefore = fixture.Projectile!.PredictionAuthorityVersion;
var wirePos = new Vector3(50f, 50f, SpawnHeight);
fixture.Controller.OnPosition(fixture.Update(
wirePos, SourceCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: true));
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
Assert.Null(canonical.RemoteMotion);
PhysicsBody body = canonical.PhysicsBody!;
Assert.Equal(spawnPose, body.Position);
Assert.Equal(spawnPose, fixture.Entity.Position);
Assert.Equal(
predictionBefore, fixture.Projectile.PredictionAuthorityVersion);
}
///
/// A2/R1's regression scenario, driven end to end: Missile bit set, but
/// no RuntimeProjectile bound (TryBind refused, or has not run
/// yet — ProjectileController.cs:160-166's unsupported-Setup
/// case, or the pre-bind window). Retail places every non-player object
/// unconditionally; the fixed classifier must classify this packet
/// Remote and route it through the SAME generic remote placement path
/// an ordinary remote uses — never the frozen silent drop the
/// unconjoined discriminator produced.
///
[Fact]
public void MissileUnbound_FallsThroughToRemoteTail_TracksInsteadOfFreezing()
{
using var fixture = new Fixture(MissileGuid, isMissile: false);
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
// Flip Missile AFTER construction, deliberately WITHOUT binding a
// RuntimeProjectile — the unbound shape A2 names.
fixture.Lifetime.Entities.SetFinalPhysicsState(
canonical,
canonical.FinalPhysicsState | PhysicsStateFlags.Missile);
Assert.Null(canonical.Projectile);
EntityPhysicsHost host = fixture.InstallHost();
fixture.Remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
Vector3 spawnPose = fixture.Entity.Position;
fixture.Controller.OnPosition(fixture.Update(
MissileAirborneDestination, DestinationCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: true));
// Placed via the ordinary remote far-snap arm — not frozen, and
// armed exactly like FarSnap_BothGuids above.
Assert.NotEqual(spawnPose, fixture.Remote.Body.Position);
Assert.Equal(fixture.Remote.Body.Position, fixture.Entity.Position);
Assert.Equal(DestinationCell, fixture.Entity.ParentCellId);
Assert.NotNull(host.PositionManager.Constraint);
fixture.DrainPlacementFifo();
}
///
/// A3/R2's adopted-body scenario: an ordinary remote (populated Interp
/// queue, an armed ConstrainTo leash) whose Missile bit is later set by
/// a State packet — ProjectileController.TryBind's shared-body
/// branch adopts the SAME RemoteMotion/body rather than replacing
/// it. Retail's teleport_hook per-manager guards are satisfied
/// for this shape, so all six actions run; the App-level pre-dispatch
/// hook call (mirroring the remote teleport arm's own
/// RunRemoteTeleportHook wiring) must un-arm the leash and clear
/// the queue before the placement, exactly like retail's ordering.
///
[Fact]
public void MissileAdoptedBody_TeleportCommit_UnConstrainsAndClearsInterpQueue()
{
using var fixture = new Fixture(MissileGuid, isMissile: false);
EntityPhysicsHost host = fixture.InstallHost();
fixture.Remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
// Arm the leash and populate the queue directly — the pre-teleport
// "live remote" state the adopted-body scenario requires.
host.PositionManager.ConstrainTo(
new AcDream.Core.Physics.Position(
SourceCell, fixture.Remote.Body.Position, Quaternion.Identity),
startDistance: 1f,
maxDistance: 5f);
Assert.NotNull(host.PositionManager.Constraint);
fixture.Remote.Interp.Enqueue(
fixture.Remote.Body.Position + Vector3.UnitX,
heading: 0f,
isMovingTo: false,
currentBodyPosition: fixture.Remote.Body.Position);
Assert.True(fixture.Remote.Interp.IsActive);
// TryBind's shared-body branch: adopt the SAME body into a
// RuntimeProjectile, and set Missile — the record now carries BOTH
// a RemoteMotion and a bound projectile, exactly like production.
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
fixture.Lifetime.Entities.SetFinalPhysicsState(
canonical,
canonical.FinalPhysicsState | PhysicsStateFlags.Missile);
fixture.Lifetime.Physics.BindProjectile(
canonical,
canonical.PhysicsBody!,
new ProjectileCollisionSphere(Vector3.Zero, 0.1f, 1f));
Assert.NotNull(canonical.Projectile);
Assert.NotNull(canonical.RemoteMotion);
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
fixture.Controller.OnPosition(fixture.Update(
MissileAirborneDestination, DestinationCell, teleportSequence: 5,
guid: MissileGuid, isGrounded: true));
// UnConstrain and StopInterpolating both ran. UnConstrain unmarks
// IsConstrained rather than nulling the manager back out (it was
// lazily CREATED by the ConstrainTo test-setup call above, and
// creation is one-way) — this is the exact discriminator
// WireAirborneNullClassified_BothGuids_WritesOnlyAP135Bookkeeping's
// own comment names ("Constraint is lazily created only on a
// genuine arm").
Assert.NotNull(host.PositionManager.Constraint);
Assert.False(host.PositionManager.Constraint!.IsConstrained);
Assert.False(fixture.Remote.Interp.IsActive);
// The placement itself still committed through the projectile arm.
Assert.Equal(
MissileAirborneDestination + DestinationWorldOffset,
canonical.PhysicsBody!.Position);
fixture.DrainPlacementFifo();
}
///
/// B1/B2 fix (round-2 review): the far-branch counterpart to
/// .
/// Retail's far branch (SetPositionSimple,
/// player_distance >= 96 m) runs StopInterpolating
/// @0x005163C9-@0x005163CB whenever position_manager != 0 — the
/// SAME guard the teleport branch's full teleport_hook shares
/// for its own StopInterpolating action — but the far branch
/// does NOT run the other five teleport_hook actions (no
/// UnConstrain). For a bare missile this is structurally inert
/// (no RemoteMotion), which is why the pre-round-2 AP-141 row
/// could call the far-branch skip "faithful by consequence." The
/// adopted-body case breaks that: it carries a live Interp
/// queue the far branch must ALSO clear, while its armed
/// ConstrainTo leash must stay armed (proving the far branch
/// really does run only StopInterpolating, not the full hook).
///
///
/// Round-3 nit (C2): "leash still armed" pins acdream's OWN divergence,
/// not retail's behaviour. Retail's HandleReceivedPosition
/// @0x00454254/@0x00454272 re-anchors an existing leash at the object's
/// just-updated position on every nonzero MoveOrTeleport return —
/// including the far branch's. acdream's far arm never calls
/// ConstrainTo at all (D-P4, AP-141 clause (b)), so "still armed"
/// here means "left exactly as staged," not "correctly re-anchored." A
/// future reader should not read this assertion as full far-branch
/// leash fidelity — only the StopInterpolating half is ported.
///
///
[Fact]
public void MissileAdoptedBody_FarCommit_ClearsInterpQueueButLeavesConstraintArmed()
{
using var fixture = new Fixture(MissileGuid, isMissile: false);
EntityPhysicsHost host = fixture.InstallHost();
fixture.Remote.Body.TransientState = TransientStateFlags.Active
| TransientStateFlags.Contact;
host.PositionManager.ConstrainTo(
new AcDream.Core.Physics.Position(
SourceCell, fixture.Remote.Body.Position, Quaternion.Identity),
startDistance: 1f,
maxDistance: 5f);
Assert.NotNull(host.PositionManager.Constraint);
Assert.True(host.PositionManager.Constraint!.IsConstrained);
fixture.Remote.Interp.Enqueue(
fixture.Remote.Body.Position + Vector3.UnitX,
heading: 0f,
isMovingTo: false,
currentBodyPosition: fixture.Remote.Body.Position);
Assert.True(fixture.Remote.Interp.IsActive);
Assert.True(fixture.Lifetime.Entities.TryGetActive(
MissileGuid, out RuntimeEntityRecord canonical));
fixture.Lifetime.Entities.SetFinalPhysicsState(
canonical,
canonical.FinalPhysicsState | PhysicsStateFlags.Missile);
fixture.Lifetime.Physics.BindProjectile(
canonical,
canonical.PhysicsBody!,
new ProjectileCollisionSphere(Vector3.Zero, 0.1f, 1f));
Assert.NotNull(canonical.Projectile);
Assert.NotNull(canonical.RemoteMotion);
fixture.PublishDestinationCollision();
fixture.ServiceWindow.Allow(DestinationLandblock);
// teleportSequence: 1 (unchanged from the fixture's baseline) with a
// cross-landblock destination classifies as the FAR disposition
// (SetPositionSimple) — the same discriminator
// MissileFarCommit_PlacesBodyNoRemoteMotionParentCellIdAgreesWithBody
// uses above, just against the adopted-body shape instead of a bare
// missile.
fixture.Controller.OnPosition(fixture.Update(
MissileAirborneDestination, DestinationCell, teleportSequence: 1,
guid: MissileGuid, isGrounded: true));
// StopInterpolating ran — the queue is cleared.
Assert.False(fixture.Remote.Interp.IsActive);
// UnConstrain did NOT run — the far branch is one action, not six.
// The leash is still armed.
Assert.NotNull(host.PositionManager.Constraint);
Assert.True(host.PositionManager.Constraint!.IsConstrained);
// The placement itself still committed through the projectile arm.
Assert.Equal(
MissileAirborneDestination + DestinationWorldOffset,
canonical.PhysicsBody!.Position);
fixture.DrainPlacementFifo();
}
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!;
internal LiveEntityAnimationState? Animated { get; private set; }
///
/// C4 route 5 (A5 fix): the bound projectile component for an
/// isMissile fixture — for an ordinary
/// remote fixture. Exposed so tests can assert prediction-version
/// movement without re-deriving it from .
///
internal RuntimeProjectile? Projectile { get; private set; }
private readonly GpuWorldState _spatial;
private readonly uint _guid;
private readonly bool _nullClassification;
///
/// C5b review L1/L2: true once the hydration controller's materializer
/// was actually reached and DECLINED. Only meaningful when the fixture
/// was built with decliningMaterializer: true.
///
internal bool MaterializerDeclined => _decliningMaterializer?.Declined
?? false;
private readonly DecliningMaterializer? _decliningMaterializer;
internal Fixture(
uint guid,
bool nullClassification = false,
bool withAnimation = false,
bool isMissile = false,
bool decliningMaterializer = false)
{
_guid = guid;
_nullClassification = nullClassification;
_decliningMaterializer =
decliningMaterializer ? new DecliningMaterializer() : null;
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();
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);
// C4 route 5 (A5 fix): a missile fixture carries the Missile bit
// from spawn — the SAME data-driven bit
// RuntimeEntityObjectLifetime.ClassifyRemoteAcceptedPosition
// (D-P1) reads, so this fixture's record classifies exactly like
// a real missile would once TryBind/BindProjectile below binds
// the component.
PhysicsStateFlags baseState = PhysicsStateFlags.ReportCollisions
| (isMissile ? PhysicsStateFlags.Missile : PhysicsStateFlags.None);
var physics = new PhysicsSpawnData(
RawState: (uint)baseState,
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,
"onposition-collapse-fixture",
null,
null,
0x09000001u,
PhysicsState: (uint)baseState,
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));
// C4 route 5 (A5 fix): a missile fixture binds a
// RuntimeProjectile directly through the SAME production Runtime
// entry point ProjectileController.TryBind eventually calls
// (RuntimePhysicsState.BindProjectile) — never a RemoteMotion.
// This is the exact shape invariant 8's mutual exclusion pins:
// Missile-set means a projectile arm and NO RemoteMotion ever
// exists for the entity.
if (isMissile)
{
var body = new PhysicsBody
{
Position = Entity.Position,
Orientation = Entity.Rotation,
LastUpdateTime = 1d,
State = baseState,
TransientState = TransientStateFlags.Active,
};
body.SnapToCell(SourceCell, Entity.Position, Entity.Position);
RuntimeEntityRecord canonical = record.Canonical!;
Lifetime.Entities.SetPhysicsBody(canonical, body);
canonical.ObjectClock.Activate();
Lifetime.Physics.AcknowledgeSpatialProjection(canonical, spatial: true);
Projectile = (RuntimeProjectile)Lifetime.Physics.BindProjectile(
canonical, body, new ProjectileCollisionSphere(Vector3.Zero, 0.1f, 1f));
Shadows.Register(
Entity.Id,
0x02000001u,
Entity.Position,
Entity.Rotation,
radius: 0.1f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: SourceLandblock,
collisionType: ShadowCollisionType.Sphere,
state: (uint)baseState,
seedCellId: SourceCell,
isStatic: false);
}
else
{
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));
LiveEntityAnimationRuntimeView animatedEntities;
if (withAnimation)
{
var slot = new LiveEntityRuntimeSlot();
slot.Bind(Runtime);
animatedEntities =
new LiveEntityAnimationRuntimeView(slot);
// A working (style, motion) -> animation table for BOTH
// Ready and WalkForward, so SetCycle's dispatch through
// PerformMovement genuinely succeeds and CurrentMotion
// actually changes — a bare `new MotionTable()` has no
// cycles, so every SetCycle silently no-ops
// (AnimationSequencer.SetCycle: "Failed dispatch ... leaves
// sequence AND state untouched"), which would make every
// scenario 8 assertion vacuously true regardless of the
// collapse. Pattern mirrors
// tests/AcDream.Core.Tests/Physics/AnimationSequencerTests.cs's
// own `Fixtures.MakeMtable`.
const uint animStyle = 0x8000003Du;
const uint readyAnimId = 0x03000101u;
const uint walkAnimId = 0x03000102u;
var mt = new DatReaderWriter.DBObjs.MotionTable
{
DefaultStyle = (DRWMotionCommand)animStyle,
};
mt.StyleDefaults[(DRWMotionCommand)animStyle] =
(DRWMotionCommand)AcDream.Core.Physics.MotionCommand.Ready;
mt.Cycles[(int)((animStyle << 16)
| (AcDream.Core.Physics.MotionCommand.Ready & 0xFFFFFFu))] =
MakeMotionData(readyAnimId);
mt.Cycles[(int)((animStyle << 16)
| (AcDream.Core.Physics.MotionCommand.WalkForward & 0xFFFFFFu))] =
MakeMotionData(walkAnimId);
var loader = new FakeAnimationLoader();
loader.Register(readyAnimId, MakeTwoFrameAnim());
loader.Register(walkAnimId, MakeTwoFrameAnim());
var setup = new DatReaderWriter.DBObjs.Setup();
setup.Parts.Add(0x01000000u);
setup.DefaultScale.Add(Vector3.One);
var sequencer = new AcDream.Core.Physics.AnimationSequencer(
setup, mt, loader);
sequencer.InitializeState();
Animated = new LiveEntityAnimationState
{
Entity = Entity,
Setup = setup,
Animation = new DatReaderWriter.DBObjs.Animation(),
LowFrame = 0,
HighFrame = 0,
Framerate = 0f,
Scale = 1f,
PartTemplate = Array.Empty(),
PartAvailability = Array.Empty(),
Sequencer = sequencer,
};
animatedEntities[Entity.Id] = Animated;
}
else
{
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(),
(ILiveEntityProjectionMaterializer?)_decliningMaterializer
?? 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);
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 Update(
Vector3 destination,
uint cellId,
ushort teleportSequence,
uint guid,
bool isGrounded = true,
Vector3? velocity = null) => new(
guid,
new CreateObject.ServerPosition(
cellId,
destination.X,
destination.Y,
destination.Z,
1f, 0f, 0f, 0f),
Velocity: velocity,
PlacementId: null,
IsGrounded: isGrounded,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: teleportSequence,
ForcePositionSequence: 0);
private static DatReaderWriter.Types.MotionData MakeMotionData(uint animId)
{
var md = new DatReaderWriter.Types.MotionData();
md.Anims.Add(new DatReaderWriter.Types.AnimData
{
AnimId = (QualifiedDataId)animId,
LowFrame = 0,
HighFrame = -1,
Framerate = 30f,
});
return md;
}
private static DatReaderWriter.DBObjs.Animation MakeTwoFrameAnim()
{
var anim = new DatReaderWriter.DBObjs.Animation();
var pf0 = new DatReaderWriter.Types.AnimationFrame(1u);
var pf1 = new DatReaderWriter.Types.AnimationFrame(1u);
pf0.Frames.Add(new DatReaderWriter.Types.Frame
{
Origin = Vector3.Zero,
Orientation = Quaternion.Identity,
});
pf1.Frames.Add(new DatReaderWriter.Types.Frame
{
Origin = Vector3.Zero,
Orientation = Quaternion.Identity,
});
anim.PartFrames.Add(pf0);
anim.PartFrames.Add(pf1);
return anim;
}
private sealed class FakeAnimationLoader : IAnimationLoader
{
private readonly Dictionary _anims = new();
internal void Register(uint id, DatReaderWriter.DBObjs.Animation anim) =>
_anims[id] = anim;
public DatReaderWriter.DBObjs.Animation? LoadAnimation(uint id) =>
_anims.TryGetValue(id, out var a) ? a : null;
}
internal EntityPhysicsHost ArmSticky(uint stickTargetGuid)
{
EntityPhysicsHost host = InstallHost();
host.PositionManager.StickTo(stickTargetGuid, radius: 1f, height: 1f);
Assert.NotEqual(0u, host.PositionManager.GetStickyObjectId());
return host;
}
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) { }
}
///
/// C5b review L1/L2: models the post-C5b
/// DatLiveEntityProjectionMaterializer self-projection branch
/// DECLINING (DatLiveEntityProjectionMaterializer.cs:767-787's
/// expectedCanonical.FullCellId != 0u gate not being satisfied),
/// which is the state C5b's contract §14 item (1) says production now
/// reaches. Records that it was reached so the test cannot pass
/// vacuously, and returns — the value
/// OnPosition's recovery call site deliberately ignores.
///
private sealed class DecliningMaterializer
: ILiveEntityProjectionMaterializer
{
internal bool Declined { get; private set; }
public bool TryMaterialize(
RuntimeEntityRecord expectedCanonical,
WorldSession.EntitySpawn canonicalSpawn,
LiveProjectionPurpose purpose,
ulong expectedCreateIntegrationVersion,
AcDream.App.Rendering.LiveEntityAppearanceUpdateState?
appearanceUpdate = null)
{
Declined = true;
return false;
}
public void ResetSessionState() => Declined = false;
}
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 OnLocalPlayerFirstEntryCompleted() { }
public void ArmLoginTunnel() { }
public void ResetSession() { }
public void ResetGenerationPresentation() { }
}
private sealed class StubLocalPlayerControllerSource
: IRuntimeLocalPlayerControllerSource
{
public PlayerMovementController? Controller { get; } =
new PlayerMovementController(new PhysicsEngine());
}
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() { }
}
}
}