acdream/tests/AcDream.App.Tests/Physics/RemotePlacementDriveFixture.cs
Erik 7f1c1f5aa6 feat(physics): C4 route 4b-2 — remote far snap through the canonical placement
Flips the SetPositionSimple classification (contact, PlayerDistance >= 96 m) for
remotes onto 4b-1's drive controller and deletes both legacy far blocks, both
duplicated 96f/4f constant pairs, and both `?? Vector3.Zero` fabrications. The
4 m constant now exists exactly once. Teleport and cell-less stay legacy for
4b-3.

Retail: MoveOrTeleport @0x00516330's far branch runs StopInterpolating
@0x005163CB before SetPositionSimple @0x005163D9 and returns 1 @0x005163E8
regardless — the SetPositionError is discarded — so HandleReceivedPosition arms
ConstrainTo @0x00454272 post-move on commit AND on failure. The x87 parity
decode at @0x00516393-@0x0051639E puts exactly 96.0 on the far branch.
SetPositionSimple @0x005162B0 builds flags 0x1012 at @0x005162C4.

Non-commit outcomes still advance the body, because retail's SetPositionInternal
@0x00515BD0 commits the destination via store_position @0x00515CE2 when no cell
resolves. The partition is by STAGE, not heuristic, enforced by an exhaustive
switch: Refused/Contention/NotApplicable/RejectedPreparation store (the placement
never executed); Committed/Deferred/RejectedByPlacement do not (the engine ran
and refused, matching retail's non-storing returns @0x00515CB2 and @0x00515CD5).
Without this a refused far snap froze the remote with an emptied queue.

Also fixes a shipped defect this route made live: ParkDeferred's quiescence parks
withdrew the entity (InWorld=false, clock suspended, residency removed) and were
never restorable, while Forget(restoreCancelledPark: true) runs for every
accepted Position on every entity. The restorable decision now lives inside
ParkDeferred AFTER SnapToCell, reading body.CellPosition.ObjCellId — the value
RestoreParkWithdrawal actually restores at — against every live quiescence
rather than one minimum-OperationId token. The three pre-snap fields are hoisted
into locals because SnapToCell ends with InWorld = true. ParkCollisionResidents
passes restorableOnCancel: false explicitly; the plain unplaceable park is
provably unchanged. RestoreParkWithdrawal re-tests the prefix at restore time so
a retained route-2 park cannot re-admit into a prefix that began quiescing
during the park.

CanAttemptDestination is retained as an OPTIMISATION only, with the two Core
predicates it cannot reproduce written down at the pre-flight, plus the two
properties that depend on it staying there.

Four fix rounds and eight Opus reviews. The slice was fully green at 10,990,
10,997 and 11,004 while containing real defects — a frozen remote pinned as
correct by its own test, a fallback that over-wrote on the exact retail paths
that decline to store, and a park guard incomplete on two independent axes.

Register: AP-137 (leftover classifications take AP-87's catch-up; states the
cell-less enqueue-vs-place delta deferred to 4b-3, that RejectedData is applied
anyway, and the headless divergence), AP-138 (the refusable far placement),
AP-136 narrowed to match the relocation. #309's acceptance steps rewritten —
step 5 previously asserted a recovery the code does not perform — and gated on a
new ACDREAM_PROBE_PARK=1 signal so the check cannot pass while broken.

Suite 11,009 passed / 4 skipped / 0 failed against a measured 10,968 baseline.
The 10,973 figure recorded earlier was wrong and is corrected here.

Connected gate outstanding: the two-client far-snap walk and #309.

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

218 lines
8.3 KiB
C#

using System.Numerics;
using AcDream.Content;
using AcDream.Content.Pak;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
namespace AcDream.App.Tests.Physics;
/// <summary>
/// C4 route 4b-2: a REAL
/// <see cref="RuntimeRemotePlacementDriveController"/> over a bare
/// <see cref="RuntimeEntityObjectLifetime"/>, so App-layer acceptance tests
/// drive the production far-snap path end to end instead of simulating it.
/// Mirrors <c>RuntimeRemotePlacementDriveControllerTests</c>' own fixture.
/// </summary>
internal sealed class RemotePlacementDriveFixture : IDisposable
{
internal const uint SourceLandblock = 0xB1000000u;
internal const uint SourceCell = SourceLandblock | 0x0001u;
internal const uint DestinationLandblock = 0xB2000000u;
internal const uint DestinationCell = DestinationLandblock | 0x0001u;
/// <summary>The +X world offset <see cref="PublishDestinationCollision"/>
/// gives the destination landblock, so a committed placement's world
/// position is the authored local position plus this.</summary>
internal static readonly Vector3 DestinationWorldOffset = new(192f, 0f, 0f);
internal const float SpawnHeight = 7f;
private readonly ServiceWindow _window = new();
internal RemotePlacementDriveFixture()
{
Lifetime = new RuntimeEntityObjectLifetime(FlatEngine());
Drive = new RuntimeRemotePlacementDriveController(
Lifetime,
new GameRuntimeClock(),
new UnusedCollisionSource(),
_window);
}
internal RuntimeEntityObjectLifetime Lifetime { get; }
internal RuntimeRemotePlacementDriveController Drive { get; }
internal void AllowDestination() => _window.Allow(DestinationLandblock);
/// <summary>
/// Commits the destination landblock's collision generation and observes
/// the source world frame once, so a placement into
/// <see cref="DestinationCell"/> can actually resolve.
/// </summary>
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);
}
/// <summary>
/// Registers one remote incarnation with a canonical body, its shared
/// <see cref="RemoteMotion"/>, and the accepted destination already merged
/// onto its snapshot — exactly the state
/// <c>RuntimeEntityObjectLifetime.TryApplyPosition</c> leaves behind
/// before <c>OnPosition</c> routes the packet.
/// </summary>
internal (RuntimeEntityRecord Record, RemoteMotion Remote, PhysicsBody Body)
AddRemote(uint guid, Vector3 destination)
{
RuntimeEntityRecord record = Lifetime.RegisterEntity(
Spawn(guid)).Canonical!;
Lifetime.Entities.SetFinalPhysicsState(record, PhysicsStateFlags.Gravity);
Lifetime.Entities.SetFullCell(
record, SourceCell, (SourceCell & 0xFFFF0000u) | 0xFFFFu);
var body = new PhysicsBody
{
Position = new Vector3(10f, 10f, SpawnHeight),
Orientation = Quaternion.Identity,
LastUpdateTime = 1d,
State = PhysicsStateFlags.Gravity,
TransientState = TransientStateFlags.Active,
};
body.SnapToCell(SourceCell, body.Position, body.Position);
Lifetime.Entities.SetPhysicsBody(record, body);
record.ObjectClock.Activate();
Lifetime.Physics.AcknowledgeSpatialProjection(record, spatial: true);
record.Snapshot = record.Snapshot with
{
Position = new CreateObject.ServerPosition(
DestinationCell,
destination.X,
destination.Y,
destination.Z,
1f,
0f,
0f,
0f),
};
RemoteMotion remote = Lifetime.Physics.GetOrCreateRemoteMotion(record);
// Past AP-87's firstUp hint, so the 4 m body-to-target guard is the
// condition under test rather than the first-sample one.
remote.LastServerPosTime = 1_700_000_000d;
return (record, remote, body);
}
/// <summary>
/// Stands in for the production placement-projection subscription this
/// bare fixture never wires.
/// </summary>
internal void DrainPlacementFifo()
{
while (Lifetime.Physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot head))
{
if (!Lifetime.Physics.SetPosition.AcknowledgeProjection(head.Token))
break;
}
}
internal int LiveOperationCount =>
Lifetime.Physics.CaptureOwnership().SetPositionOperationCount;
internal int RemotePlacementLedger =>
Lifetime.CaptureOwnership().RemotePlacementDrivePendingCount;
public void Dispose() => Lifetime.Dispose();
private static WorldSession.EntitySpawn Spawn(uint guid) => new(
guid,
new CreateObject.ServerPosition(
SourceCell, 10f, 10f, SpawnHeight, 1f, 0f, 0f, 0f),
SetupTableId: null,
AnimPartChanges: Array.Empty<CreateObject.AnimPartChange>(),
TextureChanges: Array.Empty<CreateObject.TextureChange>(),
SubPalettes: Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: null,
Name: "remote",
ItemType: null,
MotionState: null,
MotionTableId: 0x09000001u);
private static PhysicsEngine FlatEngine()
{
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);
return engine;
}
private sealed class ServiceWindow : 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 UnusedCollisionSource : IPreparedCollisionSource
{
public PreparedAssetPresence ProbeCollision(
PakAssetType type,
uint sourceFileId) => PreparedAssetPresence.Available;
public PreparedCollisionReadResult<FlatSetupCollision> ReadSetupCollision(
uint sourceFileId,
CancellationToken cancellationToken = default) =>
PreparedCollisionReadResult<FlatSetupCollision>.Missing;
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()
{
}
}
}