acdream/tests/AcDream.Runtime.Tests/Physics/RuntimeCollisionPrefixQuiescenceTests.CornerLandblock.cs
Erik 529e0e9d88 feat(runtime): C3c - production placement cutover: both hosts on the residence conductors (routes 1+8)
Campaign P remaining-physics-divergence, placement cutover slice C3c
(docs/plans/2026-08-02-placement-cutover.md). Both production hosts now
register every initial Create through the residence + continuation-
executor + first-entry-conductor machinery (C0-C3b):

- Graphical (route 1): RegisterEntityWithInitialResidence at Create; the
  shared RuntimeFirstEntryDriveController pumps both conductors from the
  placement-receipt flow; MaterializeProjection and RebucketLiveEntity
  are presentation-only while a residence is ACTIVE (ExecutorCompleted is
  the presentation-binding receipt); post-residence entities take the
  full legacy path including the prepare_to_enter_world clock edges.
  PlayerModeController attaches presentation to the Runtime-published
  controller; its legacy resolve/step-heights/host-construction path is
  deleted; presentation-only rollback (retail has no entry-flow rollback).
- Headless (route 8): OnSpawned registers with residence when a drive
  exists; content-less sessions keep the pre-flip direct registration;
  SynchronizeLocalPlayer/CreateController/ApplySetupStepHeights deleted;
  prepared-collision read failure is a typed AwaitingCollisionSource
  retry; far remotes outside the service window complete celless.
- RuntimeLocalPlayerMovementState.Controller setter sealed internal; all
  controller mutation flows through the publication lifecycle.

Fix slices landed within this cutover, each dual-gated:
- F1: live movement-stat/server-physics application routed through the
  Runtime ownership seam (post-logout ingest crash on the retired
  controller eliminated; RuntimeMovementSkillProjection deleted).
- F2: login activation wedge - collision-admission prefix gate factored
  out of the seal (reentrant-commit RejectedAuthority), rearm generation
  identity corrected, PlayerModeAutoEntry requires the Runtime-published
  controller (world reveal can no longer seal unmaterialized).
- F3: landblock-prefix 0-sentinel replaced by explicit absent-id guards;
  map-corner landblocks (grid row/col 0) fully legal through admission,
  park/rearm/retire, quiescence, and outdoor shadow seeds.
- F5: local-player first-entry ground contact seeded by the shared
  SpawnPlacementSettler (moved App->Core) at FinalizeActivation - the
  retail first-gravity-frame touch (enter_world 0x00516170 carries no
  seed); the legacy unconditional force-seed is overwritten by a real
  floor-found contact; airborne spawns stay airborne; outbound contact
  bit verified end-to-end. Fixes the standing-cast 'You can't do that
  while in the air!' rejections.
- R1 (dual-review round): login constraint leash armed at the committed
  placement (HandleReceivedPosition 0x00453FD0 analog); register rows
  AD-61 (settle-timing compression now covering the local player) and
  AD-42 (repointed off the deleted resolve split) in this commit;
  residence-conversion owner API; wire-landblock guards; drive-pending
  ledger in IsConverged; route attach/detach latch; executor-drain drift
  model documented + source-pinned.

Gates: Runtime 1,003, App 4,039/3 skips, Headless 79, complete solution
10,816/0 failed/4 skips (Release, -m:1); connected lifecycle/reconnect
gate PASS (logs/connected-world-gate-20260802-175401; graceful exits,
world-visible, zero airborne rejections). The nine-stop soak remains red
for the pre-existing 6b28ff99 whole-world collision-clone throughput
regression (attributed with evidence; scheduled as its own slice before
C5). Dual Opus reviews (retail-conformance + adversarial): delta PASS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 18:10:33 +02:00

356 lines
15 KiB
C#

using System.Numerics;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
namespace AcDream.Runtime.Tests.Physics;
/// <summary>
/// C3c-F3: landblock (0,0) — id 0x0000FFFF, Dereth's south-west map corner —
/// has the legitimate collision prefix 0x00000000. The prefix-0 "absent"
/// sentinel used to make every collision publication, quiescence, park, wake,
/// and retirement against that landblock throw
/// <see cref="ArgumentOutOfRangeException"/> from
/// <c>RuntimeSetPositionState.BeginCollisionPrefixQuiescence</c> (the
/// connected-gate crash at teleport destination (9,4), whose far streaming
/// radius reaches the corner: logs/connected-world-gate-20260802-135444).
/// These tests drive the exact production owner chain against the corner id
/// and, for quiescence semantics, assert step-for-step parity with a
/// nonzero-prefix landblock.
/// </summary>
public sealed partial class RuntimeCollisionPrefixQuiescenceTests
{
private const uint CornerLandblock = 0x0000FFFFu;
private const uint CornerPrefix = 0x00000000u;
private const uint CornerCell = 0x00000001u;
private const uint CornerCell2 = 0x00000002u;
private const uint CornerIndoorCell = 0x00000100u;
private const uint NeighborLandblock = 0x0001FFFFu;
[Fact]
public void CornerLandblockCollisionGenerationCommitsThroughTheProductionAdmissionChain()
{
// Empty engine — production streaming publishes the corner landblock
// from nothing, exactly like LandblockPhysicsPublisher.AdvanceCompleteOne.
using var fixture = new Fixture(
bindGeneration: true,
engine: new PhysicsEngine { DataCache = new PhysicsDataCache() });
RuntimePhysicsState physics = fixture.Lifetime.Physics;
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(CornerLandblock);
Assert.Equal(CornerLandblock, admission.LandblockId);
using PreparedLandblockCollisionGeneration prepared =
PrepareSealedMutation(physics, admission, CornerLandblock);
// Pre-fix this first commit threw ArgumentOutOfRangeException
// ("landblockId") from BeginCollisionPrefixQuiescence's prefix == 0
// sentinel guard.
RuntimeCollisionGenerationCommit commit =
CommitToCompletion(physics, admission, prepared);
Assert.True(commit.Completed);
Assert.True(physics.Engine.IsLandblockTerrainResident(CornerLandblock));
// The neighbouring landblock (0,1) — prefix 0x00010000 — publishes
// identically through the same chain.
RuntimeCollisionAdmission neighborAdmission =
physics.BeginCollisionAdmission(NeighborLandblock);
using PreparedLandblockCollisionGeneration neighborPrepared =
PrepareSealedMutation(physics, neighborAdmission, NeighborLandblock);
RuntimeCollisionGenerationCommit neighborCommit =
CommitToCompletion(physics, neighborAdmission, neighborPrepared);
Assert.True(neighborCommit.Completed);
Assert.True(
physics.Engine.IsLandblockTerrainResident(NeighborLandblock));
RuntimePhysicsOwnershipSnapshot ownership = physics.CaptureOwnership();
Assert.Equal(0, ownership.CollisionPrefixMutationCount);
Assert.Equal(0, ownership.CollisionPrefixQuiescenceCount);
Assert.Equal(0, ownership.PendingCollisionPrefixProjectionCount);
Assert.Equal(0, ownership.CollisionAdmissionCount);
}
[Fact]
public void CornerResidentParksAndRestoresAcrossAnActivationReplacement()
{
// Mirror of ActivationWaitsForExactWithdrawAndPlaceReceipts against
// the corner landblock: publish/park (ParkDeferred's quiescence
// override carries prefix 0x00000000), wake, and restore.
using var fixture = new Fixture(
bindGeneration: true,
engine: CornerEngine());
RuntimeEntityRecord record = fixture.Add(
0x700031F1u,
1,
CornerCell,
new Vector3(11f, 12f, 0f));
RuntimeSetPositionOutcome seeded = fixture.Place(
record,
CornerCell,
new Vector3(11.5f, 12f, 0f));
// SetPosition against a corner cell commits (host acknowledgement of
// the Place projection is the ordinary pending suffix, identical to
// any nonzero-prefix landblock).
Assert.Equal(
RuntimeSetPositionStatus.CommittedHostAcknowledgementPending,
seeded.Status);
Assert.True(fixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(seeded.Projection));
RuntimePhysicsState physics = fixture.Lifetime.Physics;
RuntimeCollisionAdmission admission =
physics.BeginCollisionAdmission(CornerLandblock);
using PreparedLandblockCollisionGeneration prepared =
PrepareSealedMutation(physics, admission, CornerLandblock);
RuntimeCollisionGenerationCommit first =
physics.CommitCollisionGeneration(admission, prepared);
Assert.False(first.EngineCommitted);
Assert.False(first.Completed);
Assert.False(physics.IsSpatialRoot(record));
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawn));
Assert.Equal(RuntimePlacementProjectionKind.Withdraw, withdrawn.Kind);
Assert.True(physics.SetPosition.AcknowledgeProjection(withdrawn.Token));
_ = SealMutation(physics, admission, prepared);
RuntimeCollisionGenerationCommit transferred =
physics.CommitCollisionGeneration(admission, prepared);
Assert.True(transferred.EngineCommitted);
Assert.False(transferred.Completed);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot restored));
Assert.Equal(RuntimePlacementProjectionKind.Place, restored.Kind);
Assert.True(physics.SetPosition.AcknowledgeProjection(restored.Token));
RuntimeCollisionGenerationCommit completed =
physics.CommitCollisionGeneration(admission, prepared);
Assert.True(completed.Completed);
Assert.True(physics.IsSpatialRoot(record));
Assert.Equal(CornerCell, record.FullCellId);
Assert.Equal(0, physics.CaptureOwnership().CollisionPrefixMutationCount);
Assert.Equal(
0,
physics.CaptureOwnership().CollisionPrefixQuiescenceCount);
}
[Fact]
public void CornerPrefixQuiescenceHoldsAndReleasesExactlyLikeANonzeroPrefix()
{
// Contract test 2: a parked deferral against the prefix-0 landblock
// holds and releases quiescence exactly like a nonzero-prefix
// landblock. The identical script runs against both and every step's
// observable outcome must match.
using var corner = new Fixture(
bindGeneration: true,
engine: CornerEngine());
using var control = new Fixture(bindGeneration: true);
// The corner run addresses landblock (0,0) by its canonical id
// 0x0000FFFF — the raw input 0x00000000 stays reserved for "absent"
// (see AbsentLandblockIdStillCannotBeginQuiescence).
List<string> cornerLog = RunHeldPlacementQuiescenceCycle(
corner,
CornerLandblock,
sourceCell: CornerCell,
targetCell: CornerCell2,
guid: 0x700031F2u);
List<string> controlLog = RunHeldPlacementQuiescenceCycle(
control,
PrefixP,
sourceCell: CellP,
targetCell: PrefixP | 0x0002u,
guid: 0x700031F3u);
Assert.Equal(controlLog, cornerLog);
}
[Fact]
public void CornerLandblockDemotesAndWithdrawsThroughRetirementMutations()
{
using (var demoteFixture = new Fixture(
bindGeneration: true,
engine: CornerEngine()))
{
RuntimeEntityRecord outdoor = demoteFixture.Add(
0x700031F4u,
1,
CornerCell,
new Vector3(12f, 41f, 0f));
RuntimeEntityRecord indoor = demoteFixture.Add(
0x700031F5u,
1,
CornerIndoorCell,
new Vector3(13f, 41f, 0f));
RuntimeCollisionMutationResult first = demoteFixture.Lifetime
.Physics.DemoteCollisionToTerrain(CornerLandblock);
Assert.False(first.Completed);
Assert.True(demoteFixture.Lifetime.Physics.IsSpatialRoot(outdoor));
Assert.False(demoteFixture.Lifetime.Physics.IsSpatialRoot(indoor));
Assert.True(demoteFixture.Lifetime.Physics.SetPosition
.TryPeekProjection(
out RuntimePlacementProjectionSnapshot withdrawal));
Assert.Equal(indoor.Key, withdrawal.Token.Entity);
Assert.True(demoteFixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(withdrawal.Token));
RuntimeCollisionMutationResult completed = demoteFixture.Lifetime
.Physics.DemoteCollisionToTerrain(CornerLandblock);
Assert.True(completed.Completed);
Assert.True(completed.Ready);
}
using var withdrawFixture = new Fixture(
bindGeneration: true,
engine: CornerEngine());
RuntimeEntityRecord record = withdrawFixture.Add(
0x700031F6u,
1,
CornerCell,
new Vector3(14f, 42f, 0f));
RuntimeSetPositionOutcome seeded = withdrawFixture.Place(
record,
CornerCell,
new Vector3(14.5f, 42f, 0f));
Assert.True(withdrawFixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(seeded.Projection));
RuntimePhysicsState physics = withdrawFixture.Lifetime.Physics;
RuntimeCollisionMutationResult pending =
physics.WithdrawCollision(CornerLandblock);
Assert.False(pending.Completed);
Assert.True(physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot removed));
Assert.True(physics.SetPosition.AcknowledgeProjection(removed.Token));
RuntimeCollisionMutationResult withdrawn =
physics.WithdrawCollision(CornerLandblock);
Assert.True(withdrawn.Completed);
Assert.False(withdrawn.Ready);
Assert.False(physics.IsSpatialRoot(record));
}
[Fact]
public void AbsentLandblockIdStillCannotBeginQuiescence()
{
// The prefix-0 sentinel accidentally rejected the corner landblock;
// the genuine "no landblock at all" input (id 0) must keep throwing.
using var fixture = new Fixture(
bindGeneration: true,
engine: CornerEngine());
Assert.Throws<ArgumentOutOfRangeException>(
() => fixture.Begin(0u, 2UL, includeOutdoorCells: true));
Assert.Throws<ArgumentOutOfRangeException>(
() => fixture.Lifetime.Physics.BeginCollisionAdmission(0u));
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(
CornerLandblock,
2UL,
includeOutdoorCells: true);
Assert.True(token.IsValid);
Assert.Equal(CornerPrefix, token.LandblockPrefix);
Assert.True(fixture.Lifetime.Physics.CancelCollisionPrefixQuiescence(
token));
}
/// <summary>
/// One held-placement quiescence cycle (the
/// SourceToOutsidePlacementIsHeldThenRestoredBeforeBarrierOpens shape,
/// single-prefix variant), with every observable step outcome recorded so
/// two runs can be compared for exact parity.
/// </summary>
private static List<string> RunHeldPlacementQuiescenceCycle(
Fixture fixture,
uint landblockId,
uint sourceCell,
uint targetCell,
uint guid)
{
var log = new List<string>();
RuntimeEntityRecord record = fixture.Add(
guid,
1,
sourceCell,
new Vector3(10f, 22f, 0f));
RuntimeCollisionPrefixQuiescenceToken token = fixture.Begin(
landblockId,
2UL,
includeOutdoorCells: true);
log.Add($"tokenValid={token.IsValid}");
RuntimeSetPositionOutcome held = fixture.Place(
record,
targetCell,
new Vector3(14f, 22f, 0f),
currentCell: sourceCell);
log.Add($"place={held.Status}");
log.Add($"placeGeneration={held.Projection.CollisionGeneration}");
log.Add($"placeCellLow={held.ExactCellId & 0xFFFFu:X4}");
log.Add(
$"root={fixture.Lifetime.Physics.IsSpatialRoot(record)}");
log.Add($"ackWithdraw={fixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(held.Projection)}");
log.Add($"acquire1={fixture.TryAcquire(token, out _)}");
log.Add($"acquire2={fixture.TryAcquire(token, out _)}");
log.Add($"cancelRestorePending={fixture.Lifetime.Physics
.CancelCollisionPrefixQuiescence(
token,
successorGeneration: 1UL,
successorReady: true)}");
bool peeked = fixture.Lifetime.Physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot restored);
log.Add($"restorePeeked={peeked}");
log.Add($"restoreKind={restored.Kind}");
log.Add($"restoreCellLow={restored.Token.ExactCellId & 0xFFFFu:X4}");
log.Add($"ackRestore={fixture.Lifetime.Physics.SetPosition
.AcknowledgeProjection(restored.Token)}");
log.Add($"cancelCompleted={fixture.Lifetime.Physics
.CancelCollisionPrefixQuiescence(
token,
successorGeneration: 1UL,
successorReady: true)}");
log.Add($"finalCellLow={record.FullCellId & 0xFFFFu:X4}");
log.Add(
$"finalRoot={fixture.Lifetime.Physics.IsSpatialRoot(record)}");
RuntimePhysicsOwnershipSnapshot ownership =
fixture.Lifetime.Physics.CaptureOwnership();
log.Add($"quiescences={ownership.CollisionPrefixQuiescenceCount}");
log.Add(
$"pendingProjections={ownership.PendingCollisionPrefixProjectionCount}");
return log;
}
private static RuntimeCollisionGenerationCommit CommitToCompletion(
RuntimePhysicsState physics,
RuntimeCollisionAdmission admission,
PreparedLandblockCollisionGeneration prepared)
{
for (int poll = 0; poll < 10_000; poll++)
{
RuntimeCollisionGenerationCommit commit =
physics.CommitCollisionGeneration(admission, prepared);
if (commit.Completed)
return commit;
while (physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot projection))
{
Assert.True(physics.SetPosition.AcknowledgeProjection(
projection.Token));
}
if (!commit.EngineCommitted)
_ = SealMutation(physics, admission, prepared);
}
throw new InvalidOperationException(
"Collision generation did not complete its mutation transaction.");
}
private static PhysicsEngine CornerEngine()
{
var engine = new PhysicsEngine
{
DataCache = new PhysicsDataCache(),
};
AddFlatLandblock(engine, CornerPrefix);
return engine;
}
}