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>
This commit is contained in:
parent
1b631f127d
commit
7f1c1f5aa6
24 changed files with 5234 additions and 375 deletions
|
|
@ -1,4 +1,4 @@
|
|||
using System.Net;
|
||||
using System.Net;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
|
|
@ -28,6 +28,30 @@ public sealed class RuntimeAcceptedPositionDriveControllerTests
|
|||
private const uint SpawnLandblock = 0x01010000u;
|
||||
private const float SpawnHeight = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// A landblock whose collision generation is deliberately NEVER committed
|
||||
/// in this fixture — the same id the deferred-park tests below use. The
|
||||
/// pre-engine quiescence check runs ahead of the engine and therefore
|
||||
/// ahead of any collision-readiness question, so its readiness is
|
||||
/// irrelevant to the round-3 destination-quiescence test.
|
||||
/// </summary>
|
||||
private const uint DestinationLandblock = 0x02020000u;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="SpawnLandblock"/>'s +X neighbour: landblock ids pack the
|
||||
/// block X index in bits 24-31, so 0x0101 → 0x0201 is one block east, and
|
||||
/// <c>LandDefs.LcoordToGid</c> re-derives exactly this prefix for a global
|
||||
/// lcoord one cell past the 192 m seam.
|
||||
/// </summary>
|
||||
private const uint NeighbourLandblock = 0x02010000u;
|
||||
|
||||
/// <summary>
|
||||
/// Cell (7, 0) of <see cref="SpawnLandblock"/> — block-local X in
|
||||
/// [168, 192), Y in [0, 24). <c>LandDefs.GidToLcoord</c>'s inverse:
|
||||
/// <c>low = (ly & 7) + ((lx & 7) << 3) + 1 = 0 + 56 + 1 = 57</c>.
|
||||
/// </summary>
|
||||
private const uint SpawnSeamCell = SpawnLandblock | 57u;
|
||||
|
||||
[Fact]
|
||||
public void NotApplicable_WhenDispositionIsNotForcePosition()
|
||||
{
|
||||
|
|
@ -830,6 +854,359 @@ public sealed class RuntimeAcceptedPositionDriveControllerTests
|
|||
AssertConverged(runtime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C4 route 4b-2, round-3 correction A2 (test 1 of 2) — the LOCAL PLAYER
|
||||
/// traverses the shared-core quiescence-park change, on the shape that is
|
||||
/// actually reachable through this route.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Reachability, measured rather than assumed.</b> The correction asked
|
||||
/// for a park under a quiescing SOURCE, reached through
|
||||
/// <c>PlacementTouchesPrefix</c>'s <c>CurrentCellId</c> arm. That shape is
|
||||
/// not constructible: the merge this route requires
|
||||
/// (<c>RuntimeEntityObjectLifetime.TryApplyPosition</c>) commits the
|
||||
/// accepted wire cell to <c>record.FullCellId</c> BEFORE the drive is
|
||||
/// called, and <c>CurrentCellId</c> is read from that field — so by submit
|
||||
/// time it names the DESTINATION, not the landblock being left. The
|
||||
/// reachable pre-sweep shape is therefore a quiescing destination; the
|
||||
/// reachable post-sweep shape is a quiescing swept neighbour, which is the
|
||||
/// sibling test below.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// A quiescing destination is the one shape where the rollback genuinely
|
||||
/// would re-admit a spatial root into the prefix that is trying to
|
||||
/// quiesce, so <c>ParkDeferred</c> declines it — AP-136's stated reason,
|
||||
/// applied to the cell the restore would actually use. Route 2 has no
|
||||
/// destination pre-flight of its own (unlike the remote far arm's
|
||||
/// <c>CanAttemptDestination</c>), so it reaches this unmasked.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QuiescingDestinationPrefix_ForcePositionParkIsNotRestored()
|
||||
{
|
||||
using StartedRuntime started = StartRuntime();
|
||||
GameRuntime runtime = started.Runtime;
|
||||
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
||||
EnterLocalPlayer(runtime);
|
||||
PhysicsBody body = Assert.IsType<PhysicsBody>(record.PhysicsBody);
|
||||
RuntimeAcceptedPositionDriveController drive =
|
||||
CreateAcceptedPositionDrive(runtime, out _);
|
||||
|
||||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionPrefixQuiescence(
|
||||
DestinationLandblock,
|
||||
collisionGeneration: 2UL,
|
||||
includeOutdoorCells: true);
|
||||
|
||||
WorldSession.EntityPositionUpdate correction = ForceUpdate(
|
||||
new Vector3(10f, 10f, SpawnHeight),
|
||||
landblockId: DestinationLandblock | 0x0001u);
|
||||
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps timestamps) =
|
||||
MergeAccepted(runtime, controller, correction);
|
||||
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeAcceptedPositionExecutionStatus.DeferredCell,
|
||||
drive.TryExecuteAcceptedLocalPosition(
|
||||
record,
|
||||
correction,
|
||||
disposition,
|
||||
timestamps,
|
||||
timestamps.PreviousTeleport));
|
||||
Assert.False(body.InWorld);
|
||||
|
||||
// ACE's next ordinary broadcast, ~100-200 ms later. Its merge-time
|
||||
// Forget cancels the park — and the rollback must decline, because the
|
||||
// only cell it could restore into is the retiring one.
|
||||
MergeAccepted(
|
||||
runtime,
|
||||
controller,
|
||||
OrdinaryUpdate(new Vector3(31f, 33f, SpawnHeight), positionSequence: 3));
|
||||
|
||||
Assert.False(runtime.EntityObjects.Physics.IsSpatialRoot(record));
|
||||
Assert.False(body.InWorld);
|
||||
Assert.False(record.ObjectClock.IsActive);
|
||||
|
||||
drive.Advance();
|
||||
Assert.Equal(0, drive.PendingCount);
|
||||
AssertConverged(runtime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C4 route 4b-2, round-3 correction A2 (test 2 of 2) — the reachable
|
||||
/// RESTORABLE quiescence park on the local-player route.
|
||||
///
|
||||
/// <para>
|
||||
/// The correction is placed one sphere radius inside the seam between
|
||||
/// <see cref="SpawnLandblock"/> and its +X neighbour, so
|
||||
/// <c>CellTransit.AddAllOutsideCells</c> adds the neighbour's cells to the
|
||||
/// sweep footprint (<c>AddOutsideCell</c> re-derives the block id from the
|
||||
/// global lcoord and has no same-block filter). Core's
|
||||
/// <c>ResultTouchesPrefix</c> scans every <c>QueriedCellIds</c> entry, so
|
||||
/// a healthy, resident, about-to-COMMIT ForcePosition is rewritten to
|
||||
/// <c>DeferredCell</c> by a quiescence in a landblock the player is
|
||||
/// neither in nor going to.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Before the shared-core change every quiescence park was non-restorable,
|
||||
/// so the next packet's merge-time <c>Forget</c> destroyed the only
|
||||
/// operation able to wake the player and left it <c>InWorld = false</c>,
|
||||
/// clock suspended, not a spatial root — session-permanent, because
|
||||
/// nothing else restores that state. The three asserts at the end are that
|
||||
/// defect. Restoring is safe here for the reason
|
||||
/// <c>ParkDeferred</c> tests directly: the cell it would restore into is
|
||||
/// the destination, and the destination is not quiescing.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QuiescingSweptNeighbour_ForcePositionParkIsRestoredByTheNextPacket()
|
||||
{
|
||||
using StartedRuntime started = StartRuntime();
|
||||
GameRuntime runtime = started.Runtime;
|
||||
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
||||
EnterLocalPlayer(runtime);
|
||||
CommitLandblockCollision(
|
||||
runtime, NeighbourLandblock, worldOffsetX: 192f);
|
||||
PhysicsBody body = Assert.IsType<PhysicsBody>(record.PhysicsBody);
|
||||
RuntimeAcceptedPositionDriveController drive =
|
||||
CreateAcceptedPositionDrive(runtime, out _);
|
||||
|
||||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionPrefixQuiescence(
|
||||
NeighbourLandblock,
|
||||
collisionGeneration: 2UL,
|
||||
includeOutdoorCells: true);
|
||||
// Neither the source nor the destination is quiescing — the whole
|
||||
// point of the shape.
|
||||
Assert.False(
|
||||
runtime.EntityObjects.Physics.SetPosition.IsCollisionPrefixQuiescing(
|
||||
SpawnLandblock));
|
||||
|
||||
// Block-local X = 191.95 m: inside cell (7, 0), 0.05 m from the 192 m
|
||||
// seam, well inside the local player's own sphere radius, which is the
|
||||
// `pointX > CellLength - radius` test AddAllOutsideCells applies
|
||||
// before adding lx + 1.
|
||||
WorldSession.EntityPositionUpdate correction = ForceUpdate(
|
||||
new Vector3(191.95f, 10f, SpawnHeight),
|
||||
landblockId: SpawnSeamCell);
|
||||
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps timestamps) =
|
||||
MergeAccepted(runtime, controller, correction);
|
||||
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeAcceptedPositionExecutionStatus.DeferredCell,
|
||||
drive.TryExecuteAcceptedLocalPosition(
|
||||
record,
|
||||
correction,
|
||||
disposition,
|
||||
timestamps,
|
||||
timestamps.PreviousTeleport));
|
||||
Assert.False(body.InWorld);
|
||||
|
||||
MergeAccepted(
|
||||
runtime,
|
||||
controller,
|
||||
OrdinaryUpdate(new Vector3(31f, 33f, SpawnHeight), positionSequence: 3));
|
||||
|
||||
Assert.True(body.InWorld);
|
||||
Assert.True(record.ObjectClock.IsActive);
|
||||
Assert.True(runtime.EntityObjects.Physics.IsSpatialRoot(record));
|
||||
|
||||
drive.Advance();
|
||||
Assert.Equal(0, drive.PendingCount);
|
||||
AssertConverged(runtime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C4 route 4b-2, round-4 correction (the MAJOR) — the DISCRIMINATING
|
||||
/// test. Round 3 relocated the restorable-park decision out of the two
|
||||
/// <c>SubmitPreparedPlacementCore</c> call sites and into
|
||||
/// <c>ParkDeferred</c>, but nothing in the tree distinguished the new
|
||||
/// predicate from the one it replaced. This does.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Round 2's form</b> compared the blocking quiescence's own token
|
||||
/// against the caller's pre-snap cell:
|
||||
/// <c>blockingToken.LandblockPrefix != (result.CellId & 0xFFFF0000)</c>.
|
||||
/// <b>Round 3's form</b> tests the POST-snap cell against every live
|
||||
/// quiescence: <c>!IsCollisionPrefixQuiescing(body.CellPosition.ObjCellId)</c>.
|
||||
/// Under round 2 this test's player is left withdrawn — <c>InWorld</c>
|
||||
/// false, clock suspended, not a spatial root — for the rest of the
|
||||
/// session. That is the exact stranding AP-136's rollback exists to
|
||||
/// prevent, and it is reached without a second quiescence, purely because
|
||||
/// the two forms read different cells.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>The shape.</b> The server names a cell in the player's OWN
|
||||
/// landblock but supplies a block-local X 5 cm PAST that block's 192 m
|
||||
/// seam. A wire (cell, position) pair that disagrees is exactly the pair
|
||||
/// <c>PhysicsBody.StageDormantCellFrame</c>'s
|
||||
/// <c>LandDefs.AdjustToOutside</c> exists to distrust (#107: "never trust
|
||||
/// a server (cell, pos) pair without re-deriving the cell"), and
|
||||
/// <c>AdjustToOutside</c>'s own contract says the re-derived id may
|
||||
/// belong to a NEIGHBOUR landblock — which is what happens here. So the
|
||||
/// prefix the placement PARKED against (the player's own, mid-retirement)
|
||||
/// and the prefix residency would be RESTORED into (the healthy
|
||||
/// neighbour) are different landblocks, and only the second one is the
|
||||
/// question <c>RestoreParkWithdrawal</c> actually asks.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Route 2 is the home for it because it has no destination pre-flight:
|
||||
/// the remote far arm's <c>CanAttemptDestination</c> refuses a quiescing
|
||||
/// destination before any park is opened, so this park is unreachable
|
||||
/// there.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QuiescingOwnPrefix_SeamCrossingParkIsRestoredAtTheReDerivedNeighbourCell()
|
||||
{
|
||||
using StartedRuntime started = StartRuntime();
|
||||
GameRuntime runtime = started.Runtime;
|
||||
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
||||
EnterLocalPlayer(runtime);
|
||||
PhysicsBody body = Assert.IsType<PhysicsBody>(record.PhysicsBody);
|
||||
RuntimeAcceptedPositionDriveController drive =
|
||||
CreateAcceptedPositionDrive(runtime, out _);
|
||||
|
||||
// Streaming begins retiring/republishing the player's OWN landblock,
|
||||
// which is also the landblock the wire cell names.
|
||||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionPrefixQuiescence(
|
||||
SpawnLandblock,
|
||||
collisionGeneration: 2UL,
|
||||
includeOutdoorCells: true);
|
||||
|
||||
// Cell (7, 0) of SpawnLandblock covers block-local X in [168, 192).
|
||||
// 192.05 m is 5 cm past its block's own seam, so AdjustToOutside
|
||||
// re-derives lx = 8 + floor(192.05 / 24) = 16, i.e. block X index 2,
|
||||
// and LcoordToGid rebuilds the id as NeighbourLandblock | 1.
|
||||
WorldSession.EntityPositionUpdate correction = ForceUpdate(
|
||||
new Vector3(192.05f, 10f, SpawnHeight),
|
||||
landblockId: SpawnSeamCell);
|
||||
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps timestamps) =
|
||||
MergeAccepted(runtime, controller, correction);
|
||||
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeAcceptedPositionExecutionStatus.DeferredCell,
|
||||
drive.TryExecuteAcceptedLocalPosition(
|
||||
record,
|
||||
correction,
|
||||
disposition,
|
||||
timestamps,
|
||||
timestamps.PreviousTeleport));
|
||||
Assert.False(body.InWorld);
|
||||
|
||||
// The two cells the two predicate forms read are DIFFERENT
|
||||
// landblocks, and only the quiescing one is the caller's.
|
||||
Assert.Equal(SpawnLandblock, correction.Position.LandblockId & 0xFFFF0000u);
|
||||
Assert.Equal(
|
||||
NeighbourLandblock,
|
||||
body.CellPosition.ObjCellId & 0xFFFF0000u);
|
||||
Assert.False(
|
||||
runtime.EntityObjects.Physics.SetPosition.IsCollisionPrefixQuiescing(
|
||||
NeighbourLandblock));
|
||||
|
||||
// ACE's next ordinary broadcast cancels the park at merge time.
|
||||
MergeAccepted(
|
||||
runtime,
|
||||
controller,
|
||||
OrdinaryUpdate(new Vector3(31f, 33f, SpawnHeight), positionSequence: 3));
|
||||
|
||||
// Restored, because the cell residency is restored INTO is healthy.
|
||||
// All three of these fail under round 2's single-token form.
|
||||
Assert.True(body.InWorld);
|
||||
Assert.True(record.ObjectClock.IsActive);
|
||||
Assert.True(runtime.EntityObjects.Physics.IsSpatialRoot(record));
|
||||
|
||||
drive.Advance();
|
||||
Assert.Equal(0, drive.PendingCount);
|
||||
AssertConverged(runtime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C4 route 4b-2, round-4 correction D6 — the restorable decision is
|
||||
/// re-tested at RESTORE time, not only at park time.
|
||||
///
|
||||
/// <para>
|
||||
/// <c>ParkDeferred</c>'s decision is a snapshot. The far snap cancels its
|
||||
/// park synchronously, so that snapshot cannot go stale; route 2's park is
|
||||
/// RETAINED (<c>AwaitingCommitWake</c>) and the restore lands on the next
|
||||
/// packet's merge-time <c>Forget</c>, ~150 ms later at ACE's 5-10 Hz.
|
||||
/// Streaming opens a quiescence per landblock mutation, so the cell the
|
||||
/// rollback would restore into can start quiescing inside that window.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// This drives exactly that: a swept-NEIGHBOUR quiescence parks a
|
||||
/// placement whose destination is healthy (so the park IS restorable when
|
||||
/// taken), then the destination's own prefix begins quiescing before the
|
||||
/// next packet. The restore must decline — otherwise it re-admits a
|
||||
/// spatial root into a prefix that is trying to retire, which is AP-136's
|
||||
/// stated reason verbatim. The entity self-heals on a later packet, so
|
||||
/// declining here is strictly the conservative half.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void QuiescenceOpenedAfterTheParkDeclinesTheRestoreAtMergeTime()
|
||||
{
|
||||
using StartedRuntime started = StartRuntime();
|
||||
GameRuntime runtime = started.Runtime;
|
||||
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
||||
EnterLocalPlayer(runtime);
|
||||
CommitLandblockCollision(
|
||||
runtime, NeighbourLandblock, worldOffsetX: 192f);
|
||||
PhysicsBody body = Assert.IsType<PhysicsBody>(record.PhysicsBody);
|
||||
RuntimeAcceptedPositionDriveController drive =
|
||||
CreateAcceptedPositionDrive(runtime, out _);
|
||||
|
||||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionPrefixQuiescence(
|
||||
NeighbourLandblock,
|
||||
collisionGeneration: 2UL,
|
||||
includeOutdoorCells: true);
|
||||
|
||||
WorldSession.EntityPositionUpdate correction = ForceUpdate(
|
||||
new Vector3(191.95f, 10f, SpawnHeight),
|
||||
landblockId: SpawnSeamCell);
|
||||
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps timestamps) =
|
||||
MergeAccepted(runtime, controller, correction);
|
||||
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
|
||||
|
||||
Assert.Equal(
|
||||
RuntimeAcceptedPositionExecutionStatus.DeferredCell,
|
||||
drive.TryExecuteAcceptedLocalPosition(
|
||||
record,
|
||||
correction,
|
||||
disposition,
|
||||
timestamps,
|
||||
timestamps.PreviousTeleport));
|
||||
Assert.False(body.InWorld);
|
||||
// The park WAS restorable when it was taken: the destination prefix
|
||||
// was clean. (Its sibling test above asserts the restore that follows
|
||||
// from exactly this state.)
|
||||
Assert.Equal(
|
||||
SpawnLandblock,
|
||||
body.CellPosition.ObjCellId & 0xFFFF0000u);
|
||||
|
||||
// …and now streaming retires that very landblock, mid-park.
|
||||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionPrefixQuiescence(
|
||||
SpawnLandblock,
|
||||
collisionGeneration: 3UL,
|
||||
includeOutdoorCells: true);
|
||||
|
||||
MergeAccepted(
|
||||
runtime,
|
||||
controller,
|
||||
OrdinaryUpdate(new Vector3(31f, 33f, SpawnHeight), positionSequence: 3));
|
||||
|
||||
Assert.False(runtime.EntityObjects.Physics.IsSpatialRoot(record));
|
||||
|
||||
drive.Advance();
|
||||
Assert.Equal(0, drive.PendingCount);
|
||||
AssertConverged(runtime);
|
||||
}
|
||||
|
||||
private static void AssertConverged(GameRuntime runtime)
|
||||
{
|
||||
RuntimeEntityObjectOwnershipSnapshot ownership =
|
||||
|
|
@ -1011,7 +1388,8 @@ public sealed class RuntimeAcceptedPositionDriveControllerTests
|
|||
/// </summary>
|
||||
private static void CommitLandblockCollision(
|
||||
GameRuntime runtime,
|
||||
uint landblockId)
|
||||
uint landblockId,
|
||||
float worldOffsetX = 0f)
|
||||
{
|
||||
// Mirrors HeadlessSessionHostTests.AddFlatLandblock's exact
|
||||
// proven-working shape (every heightmap byte and every table entry
|
||||
|
|
@ -1029,7 +1407,7 @@ public sealed class RuntimeAcceptedPositionDriveControllerTests
|
|||
new TerrainSurface(heights, heightTable),
|
||||
Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(),
|
||||
worldOffsetX: 0f,
|
||||
worldOffsetX,
|
||||
worldOffsetY: 0f);
|
||||
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
||||
landblockId, 1UL, ready: true);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue