The steady-state accepted-Position merge did two things retail never does,
on every single Position packet: it installed the wire placement frame and
unparented unconditionally, and it derived the record's FullCellId from
bare wire acceptance. Both are now correct, and they land together - a
half-flipped intermediate (classified flags with the wire stamp, or vice
versa) is exactly the mixed-residency state this campaign keeps paying for.
WHY the flags need no route. SmartBox::HandleReceivedPosition @0x00453FD0
decides both pre-placement writes BEFORE MoveOrTeleport is consulted: Gate A
@0x0045400C returns @0x0045409D ahead of unset_parent @0x00454129 and ahead
of the HasAnims SetPlacementFrame gate @0x00454137. Neither gate reads the
near/far/teleport classification. So the two flags are a pure function of
(disposition, hasAnimations) and are computable inside the merge, pre-merge,
with no signature change, no route construction and no playerDistance - the
scoping's ~150-400-line route-plumbing estimate over-counted because it did
not see this. That truth table IS
RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition's own
ApplyPlacementFrameBeforeRouting/UnparentBeforeRouting rows; the classifier
stays the oracle and the equality is pinned by test, not by a shared path,
so each computation remains separately sabotage-verifiable.
WHY the cell is withheld. HandleReceivedPosition reads the wire objcell_id
into a LOCAL @0x00453FE3 and hands it only to BlipPlayer / TeleportPlayer /
MoveOrTeleport / ConstrainTo; it never assigns the object's cell. The
object's cell moves inside the placement family (SetPositionInternal
@0x00515BD0 to set_cell, enter_world) or per-frame transit, and nowhere
else. The continuation executor has encoded that rule since the executor
slice; this caller now matches it verbatim.
WHAT DELIBERATELY SURVIVES. Two steady-state wire-cell writers stay,
downstream of the merge and outside the classification window: the
OnPosition prologue rebucket (W2, into CommitRebucket), which is also the
local player's own cell-freshness path, and the post-routing wire-cell adopt
for non-placing arms (W3, AP-135). Gating W2 "for symmetry" would freeze the
player's canonical cell between teleports and #319's child-cell equality
would inherit the freeze. AD-60's rewrite names both so the retirement
cannot be misread as "wire acceptance never changes residency anywhere".
REGISTER. AP-131 RETIRED - the unconditional literals no longer exist; the
caller was corrected, not deleted, so the row's own "deleted at the
production cutover" framing is overtaken. AD-60's legacy half RETIRED and
the row REWRITTEN rather than deleted, naming W2/W3 (route 4b-3's D8
precedent: a silent whole-row deletion would hide surviving channels).
AP-130 amended - the merge consumes the same static HasAnimations proxy,
deliberately not escalated to a live animation-queue read. AP-146 and #320
amended - their "accepted inbound Position (RefreshSnapshot into
RuntimeEntityRecord.cs:234)" local-player cell writer is now the generic
tail's CommitRebucket, and a ForcePosition (which returns before that tail)
is placement-receipt-authoritative. #275 closed.
HEADLINE BEHAVIOURAL DELTA, stated once: a refused or contended local
ForcePosition now leaves FullCellId at the last committed cell where the
merge used to stamp the refused packet's wire cell. Retail cannot refuse
(AD-62) and its body keeps its last placed cell, so the new shape is the
retail-reachable one.
THREE CONSUMER SITES THE CONTRACT'S BLAST-RADIUS SURVEY MISSED, all
D2-caused, all found by the suite rather than by reading, all intended
semantics rather than regressions (recorded in the contract's new section
14):
(1) DatLiveEntityProjectionMaterializer's self-projection branch reads
FullCellId inside OnPosition's prologue recovery, ahead of W2. It now
correctly declines to project from an unplaced wire claim; 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). Two hydration tests asserted the bucket at the recovery
boundary and now drive the production W2 step - the same shape as trap
T2, one layer up.
(2) ProjectileController.SyncPresentationFromResolvedBody writes
ParentCellId = record.FullCellId. On a refused missile placement that is
now the committed source cell. The MAJOR-1 invariant is unchanged and is
now asserted as the identity it always meant rather than as a wire-cell
constant.
(3) The merge's Rebucketed ternary does NOT become always-Updated as the
contract predicted, and is deliberately kept: the
Forget(restoreCancelledPark: true) above it can roll a wakeable
lost-cell park back, and RestoreParkWithdrawal restores canonical
residency. That is a real cell edge produced inside this method by a
placement owner.
TEST-COUNT RECONCILIATION. Baseline measured at this HEAD by stashing the
change: Runtime.Tests 1176, App.Tests 4135 (4132 passed / 3 skipped),
solution 11,106 passed / 4 skipped - matching the recorded figure at
6921a027 exactly. Post-change: Runtime.Tests 1195, App.Tests 4135 unchanged,
solution 11,125 passed / 4 skipped / 0 failed. Net +19, entirely new Runtime
tests: 3 facts plus a 12-row matrix theory in
InboundPhysicsStateControllerTests, 1 fact plus a 2-row theory in the new
RuntimeSteadyStatePositionMergeTests, and 1 fact in
RuntimeAcceptedPositionDriveControllerTests. No test was deleted; five
existing tests were rewritten in place, never delete-only. No new skip; none
of #302/#308/#321 appeared.
SABOTAGE VERIFICATIONS (each new discriminating test, both directions;
production line broken, suite run, line restored):
installPlacementFrame (!force && !hasAnimations) to (!force)
5 fail: ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame plus
the 4 animated non-force matrix rows.
installPlacementFrame to false
6 fail: ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame,
PositionPlacementAbsentAndPresentZeroBothApplyRetailZero plus the 4
non-animated non-force matrix rows.
clearParent (!force) to true
3 fail: ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment
plus the 2 force+parented matrix rows.
clearParent (!force) to false
4 fail: the 4 Apply+parented matrix rows.
refreshPosition false to acceptedPosition
4 fail: AcceptedPosition_WithholdsTheWireCellAtTheMergeBoundary,
ContendedForcePosition_WritesNoResidencyAnywhere,
ReentrantNewerPositionDuringPickupDiscardSuppressesStalePickupDelta,
MissileFarRefused_...ParentCellIdAgreesWithCommittedCell. Confirmed a
second time by the baseline measurement above, where the withhold test
was the sole red.
CommitRebucket publishes Updated instead of Rebucketed
2 fail: both parent classes of
CellChangingAcceptedPosition_ConservesOneRebucketAndOneChildPropagation.
RuntimeEntityDirectory.SetFullCell drops PropagateFullCellToChildren
2 fail: the same two rows.
T4 respected: the ForcePosition placement-frame half is inert
(appliedPlacement keeps old.PlacementId under either flag value), so the
force row's discriminating assertion is parent retention, never the frame.
NOT DONE, deliberately: the executor is still not wired into the
steady-state path (#275's alternative branch); W2/W3 are untouched; no probe
added or stripped; AP-130's proxy not escalated; no while-here unification
of the two merge callsites. No automated OnPosition-level test drives the
full pickup / drop / reproject sequence (no fixture covers pickup at that
layer); the contract's connected gate recipe item 1 is the positive evidence
for it and has NOT been run - this commit is not connected-gated.
Contract: docs/research/2026-08-05-c5b-contract.md (committed here, with its
section 14 implementation outcome appended).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2488 lines
110 KiB
C#
2488 lines
110 KiB
C#
using System.Net;
|
|
using System.Numerics;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Spells;
|
|
using AcDream.Runtime.Entities;
|
|
using AcDream.Runtime.Gameplay;
|
|
using AcDream.Runtime.Physics;
|
|
using AcDream.Runtime.Session;
|
|
using AcDream.Runtime.World;
|
|
|
|
namespace AcDream.Runtime.Tests.Session;
|
|
|
|
/// <summary>
|
|
/// C4 route 2 (2026-08-03): the Runtime-owned accepted-Position execution
|
|
/// seam for a ForcePosition on an already-live local player. Fixture
|
|
/// construction mirrors <c>RuntimeLiveEntitySessionControllerTests</c>'
|
|
/// session/first-entry harness — a live <see cref="PlayerMovementController"/>
|
|
/// only exists once the local player's initial-Create residence has fully
|
|
/// drained through <see cref="RuntimeFirstEntryDriveController"/>.
|
|
/// </summary>
|
|
public sealed class RuntimeAcceptedPositionDriveControllerTests
|
|
{
|
|
private const uint PlayerGuid = 0x50000001u;
|
|
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()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(started.Runtime);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(started.Runtime, out List<byte[]> gameActions);
|
|
|
|
RuntimeAcceptedPositionExecutionStatus status =
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
ForceUpdate(new Vector3(30f, 30f, 5f)),
|
|
PositionTimestampDisposition.Apply,
|
|
Timestamps(teleport: 0),
|
|
previousTeleportSequence: 0);
|
|
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.NotApplicable, status);
|
|
Assert.Empty(gameActions);
|
|
}
|
|
|
|
[Fact]
|
|
public void NotApplicable_WhenRecordIsNotTheLocalPlayer()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
(RuntimeEntityRecord record, _) = EnterLocalPlayer(started.Runtime);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(started.Runtime, out _);
|
|
|
|
// Same record, but the drive's own local-player-guid accessor no
|
|
// longer matches it (as if it belonged to some other entity).
|
|
started.Runtime.PlayerIdentity.ServerGuid = 0x70000099u;
|
|
|
|
RuntimeAcceptedPositionExecutionStatus status =
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
ForceUpdate(new Vector3(30f, 30f, 5f)),
|
|
PositionTimestampDisposition.ForcePosition,
|
|
Timestamps(teleport: 0),
|
|
previousTeleportSequence: 0);
|
|
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.NotApplicable, status);
|
|
}
|
|
|
|
[Fact]
|
|
public void NotApplicable_WhileAnInitialCreateResidenceIsStillActive()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
runtime.PlayerIdentity.ServerGuid = PlayerGuid;
|
|
CommitLandblockCollision(runtime, SpawnLandblock);
|
|
RuntimeEntityRecord record = runtime.EntityObjects
|
|
.RegisterEntityWithInitialResidence(
|
|
Spawn(PlayerGuid), isLocalPlayer: true)
|
|
.Canonical!;
|
|
// Deliberately do NOT drain the first-entry drive: the residence
|
|
// (and hence no PhysicsBody/controller) is still open — route 1's
|
|
// job, never route 2's.
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
RuntimeAcceptedPositionExecutionStatus status =
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
ForceUpdate(new Vector3(30f, 30f, 5f)),
|
|
PositionTimestampDisposition.ForcePosition,
|
|
Timestamps(teleport: 0),
|
|
previousTeleportSequence: 0);
|
|
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.NotApplicable, status);
|
|
Assert.Empty(gameActions);
|
|
}
|
|
|
|
[Fact]
|
|
public void Rejected_WhenTheClassifierDeclinesTheFrame()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(started.Runtime);
|
|
Vector3 positionBefore = controller.Position;
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(started.Runtime, out List<byte[]> gameActions);
|
|
|
|
// RuntimeAuthoritativePositionRouteClassifier.ValidAcceptedAuthority's
|
|
// ForcePosition case requires PreviousTeleportSequence ==
|
|
// AcceptedTeleportSequence; a mismatch is a genuine data-validity
|
|
// rejection (this call site never re-derives the timestamp gate's
|
|
// own freshness rule — it is asserting the classifier's own
|
|
// independent structural check).
|
|
RuntimeAcceptedPositionExecutionStatus status =
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
ForceUpdate(new Vector3(30f, 30f, 5f)),
|
|
PositionTimestampDisposition.ForcePosition,
|
|
Timestamps(teleport: 6),
|
|
previousTeleportSequence: 5);
|
|
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Rejected, status);
|
|
Assert.Equal(positionBefore, controller.Position);
|
|
Assert.Empty(gameActions);
|
|
AssertConverged(started.Runtime);
|
|
}
|
|
|
|
[Fact]
|
|
public void Committed_MovesTheBodyPreservesHeadingAndAcksExactlyOnceAfterCommit()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
Quaternion headingBeforeCorrection = controller.BodyOrientation;
|
|
|
|
var corrected = new Vector3(30f, 32f, 5f);
|
|
WorldSession.EntityPositionUpdate wire = ForceUpdate(corrected);
|
|
// A deliberately different wire rotation than the controller's own
|
|
// heading, exactly like a real server correction carries whatever
|
|
// heading it last observed — proves the seam never applies a SECOND
|
|
// heading substitution on top of the one the upstream merge already
|
|
// performed (contract §1c: RuntimeEntityObjectLifetime.TryApplyPosition's
|
|
// forcePositionRotation argument, exercised for real below via
|
|
// MergeAccepted).
|
|
wire = wire with
|
|
{
|
|
Position = wire.Position with
|
|
{
|
|
RotationX = 0f,
|
|
RotationY = 0f,
|
|
RotationZ = 1f,
|
|
RotationW = 0f,
|
|
},
|
|
};
|
|
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps timestamps) =
|
|
MergeAccepted(runtime, controller, wire);
|
|
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
|
|
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
RuntimeAcceptedPositionExecutionStatus status =
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
wire,
|
|
disposition,
|
|
timestamps,
|
|
timestamps.PreviousTeleport);
|
|
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Committed, status);
|
|
Assert.Equal(corrected, controller.Position);
|
|
Assert.Equal(headingBeforeCorrection, controller.BodyOrientation);
|
|
Assert.Single(gameActions);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
[Fact]
|
|
public void Contention_WhenTheEntityAlreadyOwnsAnActiveOperation()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
Vector3 positionBefore = controller.Position;
|
|
WorldSession.EntityPositionUpdate wire =
|
|
ForceUpdate(new Vector3(30f, 32f, 5f));
|
|
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps timestamps) =
|
|
MergeAccepted(runtime, controller, wire);
|
|
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
|
|
|
|
// Mirrors the displaced-authority scenario the deleted
|
|
// LocalForcePositionTransaction's trailing isCurrent() covered: some
|
|
// OTHER operation is already in flight for this exact entity (e.g. a
|
|
// remote-authoritative resolve mid-transaction) when the
|
|
// ForcePosition arrives.
|
|
RuntimeEntityPlacementToken displaced = runtime.EntityObjects.Physics
|
|
.SetPosition.TryBeginExclusiveAuthoredPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.LocalAuthoritative);
|
|
Assert.True(displaced.IsValid);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
RuntimeAcceptedPositionExecutionStatus status =
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
wire,
|
|
disposition,
|
|
timestamps,
|
|
timestamps.PreviousTeleport);
|
|
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Contention, status);
|
|
Assert.Equal(positionBefore, controller.Position);
|
|
Assert.Empty(gameActions);
|
|
|
|
// Cleanup: release the displaced operation so the fixture converges.
|
|
RuntimePlacementCancellationReceipt cancellation = runtime.EntityObjects
|
|
.Physics.SetPosition.ForgetExactPlacement(displaced);
|
|
if (cancellation.IsValid)
|
|
{
|
|
runtime.EntityObjects.Physics.SetPosition
|
|
.PublishCancellation(cancellation);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// C5b (#275) proof obligation 4 — the assertion that makes AD-60's
|
|
/// legacy-half retirement mean something. A local ForcePosition whose
|
|
/// drive execution CONTENDS performs no placement, so nothing may write
|
|
/// residency for that packet: the merge no longer stamps the wire cell
|
|
/// (C5b), there is no placement receipt, and the OnPosition generic tail
|
|
/// (W2) is never reached because the force path returns before it.
|
|
/// <c>FullCellId</c> is the pre-packet committed cell throughout.
|
|
///
|
|
/// <para>
|
|
/// Retail cannot refuse at all (AD-62), and its body simply keeps the
|
|
/// last cell <c>set_cell</c> gave it — so the surviving shape here is the
|
|
/// retail-reachable one. Before C5b the merge stamped the refused
|
|
/// packet's wire cell, which is the AD-60 legacy-half divergence.
|
|
/// </para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void ContendedForcePosition_WritesNoResidencyAnywhere()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
uint committedCellBefore = record.FullCellId;
|
|
Assert.NotEqual(0u, committedCellBefore);
|
|
|
|
// A wire cell that is deliberately NOT the committed one, so the
|
|
// pre-C5b stamp would have been observable.
|
|
const uint wireCell = SpawnLandblock | 0x0002u;
|
|
Assert.NotEqual(wireCell, committedCellBefore);
|
|
WorldSession.EntityPositionUpdate wire = ForceUpdate(
|
|
new Vector3(30f, 32f, SpawnHeight),
|
|
landblockId: wireCell);
|
|
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps timestamps) =
|
|
MergeAccepted(runtime, controller, wire);
|
|
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
|
|
|
|
// (1) The merge itself wrote no residency.
|
|
Assert.Equal(committedCellBefore, record.FullCellId);
|
|
|
|
RuntimeEntityPlacementToken displaced = runtime.EntityObjects.Physics
|
|
.SetPosition.TryBeginExclusiveAuthoredPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.LocalAuthoritative);
|
|
Assert.True(displaced.IsValid);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
Assert.Equal(
|
|
RuntimeAcceptedPositionExecutionStatus.Contention,
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
wire,
|
|
disposition,
|
|
timestamps,
|
|
timestamps.PreviousTeleport));
|
|
|
|
// (2) No receipt, so no residency change from the execution either.
|
|
Assert.Equal(committedCellBefore, record.FullCellId);
|
|
Assert.Empty(gameActions);
|
|
|
|
// (3) And nothing rebounds it on the settle.
|
|
drive.Advance();
|
|
Assert.Equal(committedCellBefore, record.FullCellId);
|
|
|
|
RuntimePlacementCancellationReceipt cancellation = runtime.EntityObjects
|
|
.Physics.SetPosition.ForgetExactPlacement(displaced);
|
|
if (cancellation.IsValid)
|
|
{
|
|
runtime.EntityObjects.Physics.SetPosition
|
|
.PublishCancellation(cancellation);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void DeferredCell_ParksThenCommitsAndNeverDoubleAcksAfterTheCollisionGenerationWakes()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
// A destination landblock whose collision generation was never
|
|
// committed (mirrors RuntimeSetPositionStateTests' CrossLandblockRequest
|
|
// pattern — re-admitting an ALREADY-ready landblock through this
|
|
// simple Begin/CommitCollisionGeneration pair does not block a new
|
|
// placement; only a genuinely fresh landblock does) parks the
|
|
// operation instead of committing it. Runtime's world-frame
|
|
// resolution is arithmetic once ANY frame is observed
|
|
// (RuntimePhysicsState.TryGetWorldFrameOffset:598-617), so this
|
|
// parks on collision readiness specifically, not on an unresolved
|
|
// frame.
|
|
const uint deferredLandblock = 0x02020000u;
|
|
var deferredPosition = new Vector3(10f, 10f, SpawnHeight);
|
|
WorldSession.EntityPositionUpdate parkedUpdate = ForceUpdate(
|
|
deferredPosition,
|
|
landblockId: deferredLandblock | 0x0001u);
|
|
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps timestamps) =
|
|
MergeAccepted(runtime, controller, parkedUpdate);
|
|
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
|
|
|
|
RuntimeAcceptedPositionExecutionStatus parked =
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
parkedUpdate,
|
|
disposition,
|
|
timestamps,
|
|
timestamps.PreviousTeleport);
|
|
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.DeferredCell, parked);
|
|
Assert.Empty(gameActions);
|
|
Assert.Equal(1, drive.PendingCount);
|
|
|
|
// Advance() before the destination is ready makes no progress.
|
|
drive.Advance();
|
|
Assert.Empty(gameActions);
|
|
Assert.Equal(1, drive.PendingCount);
|
|
|
|
CommitLandblockCollision(runtime, deferredLandblock);
|
|
// Production always has a LIVE host subscription
|
|
// (RuntimePlacementProjectionSubscription) that applies-and-
|
|
// acknowledges the resubmitted Place synchronously the instant
|
|
// RetryDeferred's CommitCanonical publishes it — that is what
|
|
// promotes this controller's own Watch into an observable
|
|
// acknowledged completion (RuntimeSetPositionState.AcknowledgeProjection's
|
|
// Place branch). This bare-Runtime fixture has no host wired, so it
|
|
// stands in for that subscription exactly like
|
|
// RuntimeLiveEntitySessionControllerTests' own DrainPlacementFifo.
|
|
DrainPlacementFifo(runtime);
|
|
|
|
// A single Advance() pump resolves the deferred commit.
|
|
drive.Advance();
|
|
|
|
Assert.Equal(0, drive.PendingCount);
|
|
// World position, not the cell-local wire position: the deferred
|
|
// landblock (0x0202) sits one landblock diagonally from the spawn
|
|
// landblock (0x0101) in Runtime's world frame, a (192, 192, 0) m
|
|
// offset (RuntimePhysicsState.TryGetWorldFrameOffset).
|
|
Assert.Equal(deferredPosition + new Vector3(192f, 192f, 0f), controller.Position);
|
|
// R8 review fix (2026-08-03): the ack is gated on retail's own
|
|
// independent CanSendPositionEvent requirement (Contact + OnWalkable
|
|
// — PlayerMovementController.cs:1517). Measured directly: this
|
|
// synthetic cross-landblock jump resolves with InContact=false in
|
|
// this bare-Runtime fixture (no subsequent physics tick runs here to
|
|
// sweep the body onto the terrain it was placed exactly tangent to —
|
|
// unlike the WITHIN-landblock move
|
|
// Committed_MovesTheBodyPreservesHeadingAndAcksExactlyOnceAfterCommit
|
|
// exercises, which measures Contact=true and DOES get its ack). This
|
|
// test therefore does NOT verify the single-ack-after-wake sequence
|
|
// end to end — it only proves the non-double-ack invariant below.
|
|
// Asserting exactly one ack here would require driving a real
|
|
// physics tick after the wake to establish ground contact, which is
|
|
// out of this fixture's scope; do not report this sequence as
|
|
// ack-verified (docs/ISSUES.md #285) until a harness does that.
|
|
int acksAfterFirstResolve = gameActions.Count;
|
|
|
|
// Further pumps are no-ops — the ack must never fire twice, however
|
|
// many times Advance() is pumped (still true wants Contact to
|
|
// eventually flip and the ack to fire exactly once, whenever that
|
|
// happens).
|
|
drive.Advance();
|
|
drive.Advance();
|
|
Assert.Equal(acksAfterFirstResolve, gameActions.Count);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Round 2 unified rule, branch 1 of 3 — <b>EQUAL</b>. The parked
|
|
/// operation dies (here through the exact
|
|
/// <c>RuntimeSetPositionState.Forget</c> funnel every mid-session
|
|
/// cancellation shares: supersession, the lost-cell deadline,
|
|
/// <c>ParkCollisionResidents</c>, a generation change) while NO newer
|
|
/// accepted position has taken the authority, so the record's
|
|
/// <c>PositionAuthorityVersion</c> still equals the dead operation's own.
|
|
/// Nothing is outstanding: <c>_pending</c> clears, and the drive must NOT
|
|
/// re-issue. Re-issuing on an unchanged authority is precisely the shape
|
|
/// that produced N1's second placement + second ack.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Equal_ClearsPendingWithoutReissuingWhenNoNewerAcceptedAuthorityArrived()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
const uint deferredLandblock = 0x02020000u;
|
|
var deferredPosition = new Vector3(10f, 10f, SpawnHeight);
|
|
WorldSession.EntityPositionUpdate parkedUpdate = ForceUpdate(
|
|
deferredPosition,
|
|
landblockId: deferredLandblock | 0x0001u);
|
|
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps timestamps) =
|
|
MergeAccepted(runtime, controller, parkedUpdate);
|
|
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
|
|
|
|
RuntimeAcceptedPositionExecutionStatus parked =
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
parkedUpdate,
|
|
disposition,
|
|
timestamps,
|
|
timestamps.PreviousTeleport);
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.DeferredCell, parked);
|
|
Assert.Equal(1, drive.PendingCount);
|
|
ulong authorityAtPark = record.PositionAuthorityVersion;
|
|
Vector3 positionAtPark = controller.Position;
|
|
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
runtime.EntityObjects.Physics.SetPosition.Forget(record);
|
|
if (cancellation.IsValid)
|
|
{
|
|
runtime.EntityObjects.Physics.SetPosition
|
|
.PublishCancellation(cancellation);
|
|
}
|
|
|
|
// The precondition this branch is defined by: a bare Forget cancels
|
|
// the operation WITHOUT merging anything, so the accepted authority
|
|
// has not moved.
|
|
Assert.Equal(authorityAtPark, record.PositionAuthorityVersion);
|
|
|
|
drive.Advance();
|
|
|
|
// No re-issue: a re-issue against this still-unready destination would
|
|
// park AGAIN and leave PendingCount at 1 (that is exactly what the
|
|
// round-1 shape did here).
|
|
Assert.Equal(0, drive.PendingCount);
|
|
Assert.Equal(positionAtPark, controller.Position);
|
|
// Round 3 (2026-08-03): the packet's placement WAS begun and then
|
|
// died without committing, so retail's unconditional position event is
|
|
// owed and goes out here carrying the unchanged pose
|
|
// (SmartBox::BlipPlayer @0x00453940 discards SetPositionSimple's
|
|
// error; SmartBox::HandleReceivedPosition @0x00453FD0 acks at
|
|
// @0x00454091 regardless). Before round 3 this asserted Empty, which
|
|
// encoded the defect: neither the body moved NOR an ack left.
|
|
Assert.Single(gameActions);
|
|
|
|
// Repeated pumps stay silent — the funnel cleared, it did not park a
|
|
// retry marker for an authority nothing is waiting on, and retail
|
|
// never retries a force that failed.
|
|
drive.Advance();
|
|
drive.Advance();
|
|
Assert.Equal(0, drive.PendingCount);
|
|
Assert.Single(gameActions);
|
|
Assert.Equal(positionAtPark, controller.Position);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Round 2 unified rule, branch 2 of 3 — <b>ADVANCED, newest accepted
|
|
/// event is still a ForcePosition</b>; also the B1 regression. Exact
|
|
/// production shape: a park wakes and its <c>Place</c> is ACCEPTED, so the
|
|
/// operation leaves <c>_operations</c> but its completion is RETAINED;
|
|
/// <c>Forget</c> then early-returns
|
|
/// (<c>RuntimeSetPositionState.CancelCoreDeferred</c>'s
|
|
/// <c>_operations.Remove</c> guard) and the retained completion survives
|
|
/// the next packet's merge. That next ForcePosition therefore cannot
|
|
/// begin (<c>HasRetainedCompletion</c> → <c>Contention</c>) — and before
|
|
/// the unified funnel its correction was lost outright, because the next
|
|
/// pump consumed the OLD completion and acked the OLD pose with nothing
|
|
/// left pointing at the new one. The one-frame window is real on the
|
|
/// graphical host, whose inbound dispatch precedes <c>RetryPending</c>.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Advanced_ReissuesWhenTheNewestAcceptedEventIsStillAForcePosition()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
const uint deferredLandblock = 0x02020000u;
|
|
var firstCorrection = new Vector3(10f, 10f, SpawnHeight);
|
|
WorldSession.EntityPositionUpdate parkedUpdate = ForceUpdate(
|
|
firstCorrection,
|
|
landblockId: deferredLandblock | 0x0001u);
|
|
(PositionTimestampDisposition parkedDisposition,
|
|
AcceptedPhysicsTimestamps parkedTimestamps) =
|
|
MergeAccepted(runtime, controller, parkedUpdate);
|
|
Assert.Equal(
|
|
PositionTimestampDisposition.ForcePosition, parkedDisposition);
|
|
Assert.Equal(
|
|
RuntimeAcceptedPositionExecutionStatus.DeferredCell,
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
parkedUpdate,
|
|
parkedDisposition,
|
|
parkedTimestamps,
|
|
parkedTimestamps.PreviousTeleport));
|
|
|
|
// The park wakes and its Place is accepted, leaving a RETAINED
|
|
// completion. Deliberately no Advance() yet: this is the host frame in
|
|
// which the next packet arrives before the pump runs.
|
|
CommitLandblockCollision(runtime, deferredLandblock);
|
|
DrainPlacementFifo(runtime);
|
|
|
|
var secondCorrection = new Vector3(14f, 12f, SpawnHeight);
|
|
WorldSession.EntityPositionUpdate secondUpdate = ForceUpdate(
|
|
secondCorrection,
|
|
landblockId: deferredLandblock | 0x0001u,
|
|
positionSequence: 3,
|
|
forcePositionSequence: 2);
|
|
(PositionTimestampDisposition secondDisposition,
|
|
AcceptedPhysicsTimestamps secondTimestamps) =
|
|
MergeAccepted(runtime, controller, secondUpdate);
|
|
Assert.Equal(
|
|
PositionTimestampDisposition.ForcePosition, secondDisposition);
|
|
|
|
Assert.Equal(
|
|
RuntimeAcceptedPositionExecutionStatus.Contention,
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
secondUpdate,
|
|
secondDisposition,
|
|
secondTimestamps,
|
|
secondTimestamps.PreviousTeleport));
|
|
|
|
// One pump: consume the older completion (its own reconcile + ack),
|
|
// then re-issue the newest accepted force — which the funnel proves by
|
|
// landing the body on the SECOND correction, not the first.
|
|
drive.Advance();
|
|
|
|
Assert.Equal(0, drive.PendingCount);
|
|
Assert.Equal(
|
|
secondCorrection + new Vector3(192f, 192f, 0f),
|
|
controller.Position);
|
|
// Round 3 (2026-08-03): the former `gameActions.Count <= 2` assertion
|
|
// was deleted here. It could not fail: this fixture's
|
|
// CommitLandblockCollision adds BOTH landblocks at worldOffsetX/Y 0f
|
|
// while Runtime's world frame places the deferred landblock at
|
|
// +192/+192, so the body lands over no terrain, resolves with
|
|
// InContact=false, and retail's own CanSendPositionEvent gate
|
|
// suppresses every ack — gameActions.Count is 0 here. It was also far
|
|
// too loose to encode "at most one per packet". The real
|
|
// discriminators (body landed on the SECOND correction, PendingCount)
|
|
// remain; the ack-count contract is pinned by
|
|
// TerminalWithoutCommit_SendsExactlyOnePositionEventAndLeavesTheBodyUnmoved
|
|
// and Committed_SendsExactlyOnePositionEventAcrossTheCommitAndTheSettle,
|
|
// whose fixtures genuinely satisfy the contact gate.
|
|
|
|
// Further pumps are no-ops: the funnel settled on the Equal branch.
|
|
drive.Advance();
|
|
Assert.Equal(0, drive.PendingCount);
|
|
Assert.Equal(
|
|
secondCorrection + new Vector3(192f, 192f, 0f),
|
|
controller.Position);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Round 2 unified rule, branch 3 of 3 — <b>ADVANCED, newest accepted
|
|
/// event is an ordinary Apply</b> (N2). The park is cancelled by the
|
|
/// ordinary echo's own merge-time <c>Forget</c>, and that echo — not the
|
|
/// force — now owns the accepted pose. Re-issuing here would apply the
|
|
/// force route's <c>Teleport|Slide</c> flags to an ordinary pose, send an
|
|
/// ack retail never sends on that branch, and skip the <c>ConstrainTo</c>
|
|
/// the ordinary branch runs
|
|
/// (<c>RuntimeAuthoritativePositionRouteClassifier.cs:368-388</c>). So the
|
|
/// funnel must clear without placing and without acking.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Advanced_DoesNotReissueWhenTheNewestAcceptedEventIsAnOrdinaryApply()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
const uint deferredLandblock = 0x02020000u;
|
|
WorldSession.EntityPositionUpdate parkedUpdate = ForceUpdate(
|
|
new Vector3(10f, 10f, SpawnHeight),
|
|
landblockId: deferredLandblock | 0x0001u);
|
|
(PositionTimestampDisposition parkedDisposition,
|
|
AcceptedPhysicsTimestamps parkedTimestamps) =
|
|
MergeAccepted(runtime, controller, parkedUpdate);
|
|
Assert.Equal(
|
|
PositionTimestampDisposition.ForcePosition, parkedDisposition);
|
|
Assert.Equal(
|
|
RuntimeAcceptedPositionExecutionStatus.DeferredCell,
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
parkedUpdate,
|
|
parkedDisposition,
|
|
parkedTimestamps,
|
|
parkedTimestamps.PreviousTeleport));
|
|
Assert.Equal(1, drive.PendingCount);
|
|
ulong authorityAtPark = record.PositionAuthorityVersion;
|
|
Vector3 positionAtPark = controller.Position;
|
|
|
|
// ACE's next ordinary broadcast, ~100-200 ms later. It merges (which
|
|
// Forgets the park and advances the accepted authority) and is NEVER
|
|
// dispatched to this route — both hosts gate the dispatch on
|
|
// ForcePosition.
|
|
var ordinaryPose = new Vector3(31f, 33f, SpawnHeight);
|
|
(PositionTimestampDisposition ordinaryDisposition, _) = MergeAccepted(
|
|
runtime,
|
|
controller,
|
|
OrdinaryUpdate(ordinaryPose, positionSequence: 3));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, ordinaryDisposition);
|
|
Assert.NotEqual(authorityAtPark, record.PositionAuthorityVersion);
|
|
|
|
drive.Advance();
|
|
|
|
Assert.Equal(0, drive.PendingCount);
|
|
// The force route never ran again: the body is exactly where the
|
|
// cancelled park left it, and specifically NOT on the ordinary echo's
|
|
// pose — which is what a re-issue would have placed it on, with the
|
|
// force route's Teleport|Slide flags and no ConstrainTo.
|
|
Assert.Equal(positionAtPark, controller.Position);
|
|
Assert.NotEqual(ordinaryPose, controller.Position);
|
|
// Exactly ONE AutonomousPosition, and it belongs to the FORCE packet,
|
|
// not the ordinary echo: the force's placement was begun and died
|
|
// without committing, which retail still acknowledges
|
|
// (SmartBox::HandleReceivedPosition @0x00453FD0 acks @0x00454091 with
|
|
// whatever SetPositionSimple left the body at). The ordinary echo
|
|
// never reaches this route at all — retail's ordinary branch has no
|
|
// unconditional SendPositionEvent. Round 3 (2026-08-03) corrected this
|
|
// from Empty, which encoded the lost-ack defect.
|
|
Assert.Single(gameActions);
|
|
|
|
drive.Advance();
|
|
Assert.Equal(0, drive.PendingCount);
|
|
Assert.Equal(positionAtPark, controller.Position);
|
|
Assert.Single(gameActions);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// N1 regression (2026-08-03): ONE server correction must produce EXACTLY
|
|
/// one canonical placement and EXACTLY one outbound
|
|
/// <c>AutonomousPosition</c> — never two. The round-1 shape left a dead
|
|
/// <c>_pending</c> entry behind whenever a fresh packet's own placement
|
|
/// committed while an older park was still tracked, and the next pump
|
|
/// re-issued from that dead entry: a second placement and a second ack for
|
|
/// a single correction, which is the exact duplicate-authority class
|
|
/// <c>670f307c</c> deleted and this whole slice exists to remove.
|
|
/// </summary>
|
|
[Fact]
|
|
public void OneServerCorrectionProducesExactlyOnePlacementAndOneAck()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
// An earlier force parks on a landblock whose collision is not ready.
|
|
const uint deferredLandblock = 0x02020000u;
|
|
WorldSession.EntityPositionUpdate parkedUpdate = ForceUpdate(
|
|
new Vector3(10f, 10f, SpawnHeight),
|
|
landblockId: deferredLandblock | 0x0001u);
|
|
(PositionTimestampDisposition parkedDisposition,
|
|
AcceptedPhysicsTimestamps parkedTimestamps) =
|
|
MergeAccepted(runtime, controller, parkedUpdate);
|
|
Assert.Equal(
|
|
RuntimeAcceptedPositionExecutionStatus.DeferredCell,
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
parkedUpdate,
|
|
parkedDisposition,
|
|
parkedTimestamps,
|
|
parkedTimestamps.PreviousTeleport));
|
|
Assert.Equal(1, drive.PendingCount);
|
|
Assert.Empty(gameActions);
|
|
|
|
// THE one server correction under test: a second force, into the
|
|
// already-ready spawn landblock, so it commits synchronously. Its
|
|
// merge Forgets the park.
|
|
var corrected = new Vector3(30f, 32f, SpawnHeight);
|
|
WorldSession.EntityPositionUpdate correction = ForceUpdate(
|
|
corrected,
|
|
positionSequence: 3,
|
|
forcePositionSequence: 2);
|
|
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps timestamps) =
|
|
MergeAccepted(runtime, controller, correction);
|
|
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
|
|
|
|
Assert.Equal(
|
|
RuntimeAcceptedPositionExecutionStatus.Committed,
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
correction,
|
|
disposition,
|
|
timestamps,
|
|
timestamps.PreviousTeleport));
|
|
|
|
Assert.Equal(0, drive.PendingCount);
|
|
Assert.Single(gameActions);
|
|
Assert.Equal(corrected, controller.Position);
|
|
ulong placementsAfterCorrection = record.PlacementCommitVersion;
|
|
|
|
// Every subsequent pump must be a no-op. Before the fix the FIRST of
|
|
// these re-issued the dead park entry: PlacementCommitVersion advanced
|
|
// again and a SECOND AutonomousPosition left for the same correction.
|
|
drive.Advance();
|
|
drive.Advance();
|
|
|
|
Assert.Equal(0, drive.PendingCount);
|
|
Assert.Single(gameActions);
|
|
Assert.Equal(corrected, controller.Position);
|
|
Assert.Equal(placementsAfterCorrection, record.PlacementCommitVersion);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Round 3 blocker (2026-08-03) — <b>the terminal-without-commit ack</b>.
|
|
/// Retail attempts the placement, and if it fails the body simply does not
|
|
/// move — but the packet is acknowledged regardless and never retried:
|
|
/// <c>SmartBox::BlipPlayer</c> @0x00453940 calls
|
|
/// <c>CPhysicsObj::SetPositionSimple</c> @0x005162B0, which returns an
|
|
/// <c>enum SetPositionError</c> that other retail sites test
|
|
/// (<c>== OK_SPE</c> @0x0055605D) and that <c>BlipPlayer</c> DISCARDS;
|
|
/// <c>BlipPlayer</c> returns <c>void</c>, and its caller
|
|
/// <c>SmartBox::HandleReceivedPosition</c> @0x00453FD0 then runs
|
|
/// <c>cmdinterp->SendPositionEvent()</c> @0x00454091 unconditionally
|
|
/// before returning @0x0045409D.
|
|
///
|
|
/// Unlike
|
|
/// <see cref="Equal_ClearsPendingWithoutReissuingWhenNoNewerAcceptedAuthorityArrived"/>
|
|
/// (whose subject is the no-re-issue decision) this test's subject is the
|
|
/// ack contract itself: EXACTLY one outbound <c>AutonomousPosition</c> for
|
|
/// the failed packet, carrying whatever pose the retired operation left
|
|
/// behind, with no further placement performed by the settle. Before round
|
|
/// 3 this path sent zero — the correction never took AND the server was
|
|
/// never told it had not taken.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TerminalWithoutCommit_SendsExactlyOnePositionEventAndLeavesTheBodyUnmoved()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
// The body sits on the spawn landblock's flat terrain, so retail's own
|
|
// CanSendPositionEvent admission (Contact + OnWalkable) is genuinely
|
|
// satisfied — this fixture can observe the ack rather than silently
|
|
// measuring a suppression.
|
|
Assert.True(controller.CanSendPositionEvent);
|
|
|
|
// A force whose destination landblock has no published collision
|
|
// generation parks instead of committing.
|
|
const uint deferredLandblock = 0x02020000u;
|
|
WorldSession.EntityPositionUpdate correction = ForceUpdate(
|
|
new Vector3(10f, 10f, SpawnHeight),
|
|
landblockId: deferredLandblock | 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));
|
|
|
|
// Measured, not assumed: this park happens AFTER
|
|
// `_physics.Engine.SetPosition` has already run, so the canonical body
|
|
// already sits at the destination while the placement itself is
|
|
// withdrawn and parked. Specifically it is the engine-result deferral
|
|
// (RuntimeSetPositionState.cs, the park taken once `operation.Result`
|
|
// has been assigned) — NOT either `TryGetBlockingQuiescence` branch:
|
|
// `_collisionPrefixQuiescence` is populated only by
|
|
// `BeginCollisionPrefixQuiescence`, and this fixture's
|
|
// `CommitLandblockCollision` calls `BeginCollisionGeneration` instead,
|
|
// so that map is empty and both quiescence branches are unreachable
|
|
// here. Same side of the engine call either way; the distinction only
|
|
// matters so a future reader does not go looking in the wrong branch.
|
|
// The subject of this test is the TERMINAL SETTLE, so the
|
|
// unmoved/no-further-placement baselines are captured here, at the park.
|
|
Vector3 poseAtPark = controller.Position;
|
|
ulong placementVersionAtPark = record.PlacementCommitVersion;
|
|
|
|
// Nothing has been acknowledged yet: retail's ack follows BlipPlayer,
|
|
// and the placement has not reached any terminal outcome.
|
|
Assert.Empty(gameActions);
|
|
|
|
// The park is retired without ever committing — the exact
|
|
// RuntimeSetPositionState.Forget funnel every mid-session cancellation
|
|
// shares (supersession, the lost-cell deadline, ParkCollisionResidents,
|
|
// a generation change).
|
|
RuntimePlacementCancellationReceipt cancellation =
|
|
runtime.EntityObjects.Physics.SetPosition.Forget(record);
|
|
if (cancellation.IsValid)
|
|
{
|
|
runtime.EntityObjects.Physics.SetPosition
|
|
.PublishCancellation(cancellation);
|
|
}
|
|
|
|
drive.Advance();
|
|
|
|
// Retail's two simultaneous facts at @0x0045409D: the placement did
|
|
// not take (the settle performs no placement of its own — the body is
|
|
// exactly where the retired operation left it and no further
|
|
// placement committed), and the position event went out anyway,
|
|
// carrying that unchanged pose. Before round 3 this path sent zero.
|
|
Assert.Equal(poseAtPark, controller.Position);
|
|
Assert.Equal(placementVersionAtPark, record.PlacementCommitVersion);
|
|
Assert.Single(gameActions);
|
|
Assert.Equal(0, drive.PendingCount);
|
|
|
|
// Retail never retries a force whose placement failed, so no further
|
|
// pump may place OR acknowledge anything more for this packet.
|
|
drive.Advance();
|
|
drive.Advance();
|
|
Assert.Single(gameActions);
|
|
Assert.Equal(poseAtPark, controller.Position);
|
|
Assert.Equal(placementVersionAtPark, record.PlacementCommitVersion);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Round 3 companion invariant (2026-08-03): the terminal-without-commit
|
|
/// ack must not become a SECOND ack on the committed path. A commit runs
|
|
/// <c>ReconcileAndAcknowledge</c> and then immediately settles through the
|
|
/// same funnel, so a funnel that acknowledged unconditionally would send
|
|
/// two <c>AutonomousPosition</c> messages for one server correction — the
|
|
/// exact duplicate-ack class <c>670f307c</c> deleted. Retail sends exactly
|
|
/// one per handled packet (<c>SmartBox::HandleReceivedPosition</c>
|
|
/// @0x00453FD0 reaches <c>SendPositionEvent</c> @0x00454091 once, then
|
|
/// returns @0x0045409D).
|
|
/// </summary>
|
|
[Fact]
|
|
public void Committed_SendsExactlyOnePositionEventAcrossTheCommitAndTheSettle()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
// Within the already-published spawn landblock, so the placement
|
|
// commits synchronously AND the contact gate admits the ack.
|
|
var corrected = new Vector3(30f, 32f, SpawnHeight);
|
|
WorldSession.EntityPositionUpdate correction = ForceUpdate(corrected);
|
|
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps timestamps) =
|
|
MergeAccepted(runtime, controller, correction);
|
|
Assert.Equal(PositionTimestampDisposition.ForcePosition, disposition);
|
|
|
|
Assert.Equal(
|
|
RuntimeAcceptedPositionExecutionStatus.Committed,
|
|
drive.TryExecuteAcceptedLocalPosition(
|
|
record,
|
|
correction,
|
|
disposition,
|
|
timestamps,
|
|
timestamps.PreviousTeleport));
|
|
|
|
// One commit, one ack — not two. The settle that runs immediately
|
|
// after the commit must recognise the ack already left.
|
|
Assert.Equal(corrected, controller.Position);
|
|
Assert.Single(gameActions);
|
|
Assert.Equal(0, drive.PendingCount);
|
|
|
|
drive.Advance();
|
|
drive.Advance();
|
|
Assert.Single(gameActions);
|
|
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);
|
|
}
|
|
|
|
#region C4 route 3 - portal arm
|
|
|
|
/// <summary>
|
|
/// The main D-T2/D-T3 happy path, asserting the positive facts §8 item 2
|
|
/// requires rather than only <c>InWorld</c>/clock: the body moved to the
|
|
/// resolved destination, velocity zeroed (retail
|
|
/// <c>set_velocity(player, 0, 1)</c> @0x004541B4), the leash re-armed
|
|
/// EXACTLY ONCE at the resolved position (Inversion A — proven by
|
|
/// pre-arming at a stale anchor first, so a stale anchor surviving would
|
|
/// fail the assertion), autorun cancelled (the <c>PlayerTeleported</c>
|
|
/// port), and exactly one outbound movement event with ZERO
|
|
/// AutonomousPosition packets (route's <c>SendPositionImmediately</c> is
|
|
/// always false).
|
|
/// </summary>
|
|
/// <summary>
|
|
/// R7 review fix (2026-08-05): retail <c>CommandInterpreter::SendMovementEvent</c>
|
|
/// @0x006B4680 (<c>PlayerTeleported</c>'s tail-jump) gates on
|
|
/// <c>autonomy_level != 0</c> — under server control (this class's
|
|
/// <c>_usePositionFromServer</c>, retail's <c>UsePositionFromServer()</c>)
|
|
/// retail sends nothing. Everything else about the commit (body move,
|
|
/// leash re-arm, autorun cancel) is unaffected by autonomy; only the
|
|
/// wire send is gated.
|
|
/// </summary>
|
|
[Fact]
|
|
public void PortalCommitted_UnderServerControlSendsNoMovementEvent()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
Assert.True(runtime.CharacterOwner.TrySetAutonomyLevel(0u));
|
|
Assert.True(runtime.CharacterOwner.UsePositionFromServer);
|
|
|
|
const ushort teleportSequence = 6;
|
|
var destinationPosition = new Vector3(31f, 33f, SpawnHeight);
|
|
WorldSession.EntityPositionUpdate destinationUpdate = PortalDestinationUpdate(
|
|
destinationPosition, SpawnLandblock | 0x0001u, teleportSequence);
|
|
MergeAccepted(runtime, controller, destinationUpdate);
|
|
(RuntimePortalPlacementAuthority portal, RuntimeTeleportDestination destination) =
|
|
BeginPortal(runtime, SpawnLandblock | 0x0001u, teleportSequence, destinationUpdate);
|
|
// B8/A8 review fix: guarantees ConvergePortalHost runs even if an
|
|
// assertion below fails first - see PortalHostConvergenceGuard's doc.
|
|
using var portalHostGuard = new PortalHostConvergenceGuard(
|
|
runtime, portal.RevealGeneration, portal.Projection);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
RuntimeAcceptedPositionExecutionStatus status =
|
|
drive.TryExecuteAcceptedPortalArrival(destination, portal);
|
|
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Committed, status);
|
|
Assert.Equal(destinationPosition, controller.Position);
|
|
Assert.False(runtime.MovementOwner.AutoRunActive);
|
|
Assert.Empty(gameActions);
|
|
ConvergePortalHost(runtime, portal.RevealGeneration, portal.Projection);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
[Fact]
|
|
public void PortalCommitted_MovesBodyArmsLeashOnceCancelsAutorunAndSendsExactlyOneMovementEvent()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
|
|
// Pre-arm the leash at a STALE anchor - a re-arm that merely leaves
|
|
// the OLD anchor in place (i.e. no re-arm at all) would fail the
|
|
// ConstraintPos assertion below.
|
|
var staleAnchor = new Position(
|
|
SpawnLandblock | 0x0001u,
|
|
new Vector3(1f, 1f, SpawnHeight),
|
|
Quaternion.Identity);
|
|
controller.PositionManager!.ConstrainTo(staleAnchor, 1f, 2f);
|
|
Assert.True(controller.PositionManager.Constraint!.IsConstrained);
|
|
|
|
runtime.MovementOwner.Execute(RuntimeMovementCommand.ToggleRunLock);
|
|
Assert.True(runtime.MovementOwner.AutoRunActive);
|
|
|
|
const ushort teleportSequence = 5;
|
|
var destinationPosition = new Vector3(30f, 32f, SpawnHeight);
|
|
WorldSession.EntityPositionUpdate destinationUpdate = PortalDestinationUpdate(
|
|
destinationPosition, SpawnLandblock | 0x0001u, teleportSequence);
|
|
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps timestamps) =
|
|
MergeAccepted(runtime, controller, destinationUpdate);
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
Assert.True(timestamps.TeleportAdvanced);
|
|
|
|
(RuntimePortalPlacementAuthority portal, RuntimeTeleportDestination destination) =
|
|
BeginPortal(runtime, SpawnLandblock | 0x0001u, teleportSequence, destinationUpdate);
|
|
// B8/A8 review fix: guarantees ConvergePortalHost runs even if an
|
|
// assertion below fails first - see PortalHostConvergenceGuard's doc.
|
|
using var portalHostGuard = new PortalHostConvergenceGuard(
|
|
runtime, portal.RevealGeneration, portal.Projection);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
RuntimeAcceptedPositionExecutionStatus status =
|
|
drive.TryExecuteAcceptedPortalArrival(destination, portal);
|
|
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Committed, status);
|
|
Assert.Equal(destinationPosition, controller.Position);
|
|
Assert.Equal(Vector3.Zero, controller.BodyVelocity);
|
|
Assert.True(controller.PositionManager.Constraint!.IsConstrained);
|
|
Assert.Equal(
|
|
controller.Position,
|
|
controller.PositionManager.Constraint.ConstraintPos.Frame.Origin);
|
|
Assert.False(runtime.MovementOwner.AutoRunActive);
|
|
// Exactly one outbound wire packet total: the movement-event refresh.
|
|
// Zero AutonomousPosition - the portal route never sends one.
|
|
Assert.Single(gameActions);
|
|
ConvergePortalHost(runtime, portal.RevealGeneration, portal.Projection);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// D-T1's own safety property, proved live: a portal authority whose
|
|
/// generation the transit no longer recognizes (this generation was
|
|
/// never begun) is structurally invalid, so the arm returns
|
|
/// <c>NotApplicable</c> without writing anything - the D-T5 refusal
|
|
/// shape. A superseded token is unobtainable by construction; this
|
|
/// exercises the same <c>IsValid</c> gate a stale re-derivation would
|
|
/// fail on.
|
|
/// </summary>
|
|
[Fact]
|
|
public void PortalProducerInvalidAuthority_ArmDoesNotRunAndNothingMutates()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
Vector3 positionBefore = controller.Position;
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
var destination = new RuntimeTeleportDestination(
|
|
PlayerGuid,
|
|
InstanceSequence: 1,
|
|
PositionSequence: 2,
|
|
TeleportSequence: 5,
|
|
ForcePositionSequence: 0,
|
|
new Position(
|
|
SpawnLandblock | 0x0001u,
|
|
new Vector3(30f, 32f, SpawnHeight),
|
|
Quaternion.Identity));
|
|
// Present but structurally invalid: RevealGeneration 0 fails
|
|
// RuntimePortalPlacementAuthority.IsValid outright - the exact shape
|
|
// a stale-generation TryRegisterHostProjection re-derivation refusal
|
|
// would leave the producer holding (default).
|
|
var invalidAuthority = new RuntimePortalPlacementAuthority(
|
|
Present: true,
|
|
RevealGeneration: 0,
|
|
TeleportSequence: 5,
|
|
Projection: default);
|
|
|
|
RuntimeAcceptedPositionExecutionStatus status =
|
|
drive.TryExecuteAcceptedPortalArrival(destination, invalidAuthority);
|
|
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.NotApplicable, status);
|
|
Assert.Equal(positionBefore, controller.Position);
|
|
Assert.Empty(gameActions);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// D-T5's genuinely new edge: transit pins the FIRST accepted
|
|
/// destination per generation, while <c>BeginAcceptedPlacementCore</c>
|
|
/// validates the portal's destination cell against the LATEST merged
|
|
/// snapshot. A second local Position merging a DIFFERENT landblock
|
|
/// between the offer and the Place edge makes Begin refuse. Positive
|
|
/// half: nothing mutates and the transit stays exactly as it was
|
|
/// (D-T5's "no half-state" invariant).
|
|
/// </summary>
|
|
[Fact]
|
|
public void PortalBeginCellMismatch_RefusesWithoutMutatingBodyOrTransit()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
|
|
const ushort teleportSequence = 6;
|
|
var destinationPosition = new Vector3(30f, 32f, SpawnHeight);
|
|
WorldSession.EntityPositionUpdate destinationUpdate = PortalDestinationUpdate(
|
|
destinationPosition, SpawnLandblock | 0x0001u, teleportSequence);
|
|
MergeAccepted(runtime, controller, destinationUpdate);
|
|
(RuntimePortalPlacementAuthority portal, RuntimeTeleportDestination destination) =
|
|
BeginPortal(runtime, SpawnLandblock | 0x0001u, teleportSequence, destinationUpdate);
|
|
// B8/A8 review fix: guarantees ConvergePortalHost runs even if an
|
|
// assertion below fails first - see PortalHostConvergenceGuard's doc.
|
|
using var portalHostGuard = new PortalHostConvergenceGuard(
|
|
runtime, portal.RevealGeneration, portal.Projection);
|
|
|
|
// A SECOND local Position merges a DIFFERENT landblock after the
|
|
// portal offer/registration but before Place - record.Snapshot's
|
|
// latest accepted position no longer matches the portal's
|
|
// destination cell. SAME teleport sequence (an ordinary in-flight
|
|
// Apply, not a fresh teleport) so PhysicsTimestampGate admits it.
|
|
const uint otherLandblock = 0x02020000u;
|
|
(PositionTimestampDisposition secondDisposition, _) = MergeAccepted(
|
|
runtime,
|
|
controller,
|
|
PortalDestinationUpdate(
|
|
new Vector3(1f, 1f, SpawnHeight),
|
|
otherLandblock | 0x0001u,
|
|
teleportSequence,
|
|
positionSequence: 3));
|
|
Assert.Equal(PositionTimestampDisposition.Apply, secondDisposition);
|
|
Vector3 positionBefore = controller.Position;
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
RuntimeAcceptedPositionExecutionStatus status =
|
|
drive.TryExecuteAcceptedPortalArrival(destination, portal);
|
|
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Contention, status);
|
|
Assert.Equal(positionBefore, controller.Position);
|
|
Assert.Empty(gameActions);
|
|
// The transit is untouched by the refusal - still active, still
|
|
// holding the SAME accepted destination, not cancelled.
|
|
Assert.True(runtime.TransitOwner.IsTeleportActive);
|
|
Assert.False(runtime.TransitOwner.Snapshot.Cancelled);
|
|
ConvergePortalHost(runtime, portal.RevealGeneration, portal.Projection);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
[Fact]
|
|
public void PortalContention_WhenTheEntityAlreadyOwnsAnActiveOperation()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
Vector3 positionBefore = controller.Position;
|
|
|
|
const ushort teleportSequence = 7;
|
|
WorldSession.EntityPositionUpdate destinationUpdate = PortalDestinationUpdate(
|
|
new Vector3(30f, 32f, SpawnHeight), SpawnLandblock | 0x0001u, teleportSequence);
|
|
MergeAccepted(runtime, controller, destinationUpdate);
|
|
(RuntimePortalPlacementAuthority portal, RuntimeTeleportDestination destination) =
|
|
BeginPortal(runtime, SpawnLandblock | 0x0001u, teleportSequence, destinationUpdate);
|
|
// B8/A8 review fix: guarantees ConvergePortalHost runs even if an
|
|
// assertion below fails first - see PortalHostConvergenceGuard's doc.
|
|
using var portalHostGuard = new PortalHostConvergenceGuard(
|
|
runtime, portal.RevealGeneration, portal.Projection);
|
|
|
|
RuntimeEntityPlacementToken displaced = runtime.EntityObjects.Physics
|
|
.SetPosition.TryBeginExclusiveAuthoredPlacement(
|
|
record,
|
|
record.PositionAuthorityVersion,
|
|
RuntimeSetPositionOperationKind.LocalAuthoritative);
|
|
Assert.True(displaced.IsValid);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
RuntimeAcceptedPositionExecutionStatus status =
|
|
drive.TryExecuteAcceptedPortalArrival(destination, portal);
|
|
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.Contention, status);
|
|
Assert.Equal(positionBefore, controller.Position);
|
|
Assert.Empty(gameActions);
|
|
|
|
RuntimePlacementCancellationReceipt cancellation = runtime.EntityObjects
|
|
.Physics.SetPosition.ForgetExactPlacement(displaced);
|
|
if (cancellation.IsValid)
|
|
{
|
|
runtime.EntityObjects.Physics.SetPosition
|
|
.PublishCancellation(cancellation);
|
|
}
|
|
ConvergePortalHost(runtime, portal.RevealGeneration, portal.Projection);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// D-T2.4: a portal DeferredCell park reuses the SAME retained-operation
|
|
/// machinery the force arm uses, but on wake it commits WITHOUT the force
|
|
/// funnel's re-issue decision (trap T7 - <see cref="_newestForce"/> is
|
|
/// never touched) and WITHOUT a double commit however many times
|
|
/// <see cref="RuntimeAcceptedPositionDriveController.Advance"/> is
|
|
/// pumped afterward.
|
|
///
|
|
/// <para>Root-cause note (closed): an earlier revision of this test
|
|
/// asserted the wrong post-commit position (the FORCE arm's
|
|
/// cross-landblock +192/+192 delta) and asserted zero outbound game
|
|
/// actions after a commit that legitimately sends one. Because both
|
|
/// were WRONG, the assertion failure fired before this method's own
|
|
/// <c>ConvergePortalHost</c> cleanup call ever ran, which left the
|
|
/// portal's host projection unconverged - and <c>StartedRuntime</c>'s
|
|
/// `using`-triggered <c>Dispose()</c> then threw "hosts=1, pending=1"
|
|
/// while unwinding, MASKING the real (first) failure as a teardown
|
|
/// defect. The actual cause: an accepted Position merge with
|
|
/// <c>TeleportAdvanced</c> (this is one - #283) rebases
|
|
/// <c>RuntimePhysicsState</c>'s world-frame center onto the DESTINATION
|
|
/// landblock immediately, before the placement itself ever resolves -
|
|
/// exactly as <c>ObserveLocalWorldFrame</c>'s doc comment states ("only
|
|
/// an accepted teleport moves it afterward"). So by the time this
|
|
/// deferred park's retry runs, the destination landblock IS the frame
|
|
/// center and the correct expected offset is zero, not the FORCE
|
|
/// arm's stale-frame cross-landblock delta. No production code
|
|
/// changed to fix this; both fixes were test-assertion corrections.</para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void PortalDeferredCell_ParksThenCommitsExactlyOnceOnTheCollisionGenerationWake()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(runtime, out List<byte[]> gameActions);
|
|
|
|
const uint deferredLandblock = 0x02020000u;
|
|
var deferredPosition = new Vector3(10f, 10f, SpawnHeight);
|
|
const ushort teleportSequence = 8;
|
|
WorldSession.EntityPositionUpdate destinationUpdate = PortalDestinationUpdate(
|
|
deferredPosition, deferredLandblock | 0x0001u, teleportSequence);
|
|
(PositionTimestampDisposition disposition, AcceptedPhysicsTimestamps mergeTimestamps) =
|
|
MergeAccepted(runtime, controller, destinationUpdate);
|
|
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
|
|
// #283: RuntimePhysicsState.ObserveLocalWorldFrame rebases the world
|
|
// frame center to the DESTINATION landblock the instant an accepted
|
|
// Position merges with TeleportAdvanced (RuntimeEntityObjectLifetime
|
|
// .TryApplyPosition calls it with teleportAdvanced: timestamps
|
|
// .TeleportAdvanced) - by design, "only an accepted teleport moves
|
|
// it afterward" (RuntimePhysicsState.cs doc comment). This merge IS
|
|
// that accepted teleport, so by the time the placement itself runs,
|
|
// TryGetWorldFrameOffset(deferredLandblock) is already (0,0) - the
|
|
// frame is centered ON the destination, not still on SpawnLandblock.
|
|
Assert.True(mergeTimestamps.TeleportAdvanced);
|
|
(RuntimePortalPlacementAuthority portal, RuntimeTeleportDestination destination) =
|
|
BeginPortal(runtime, deferredLandblock | 0x0001u, teleportSequence, destinationUpdate);
|
|
// B8/A8 review fix: guarantees ConvergePortalHost runs even if an
|
|
// assertion below fails first - see PortalHostConvergenceGuard's doc.
|
|
using var portalHostGuard = new PortalHostConvergenceGuard(
|
|
runtime, portal.RevealGeneration, portal.Projection);
|
|
|
|
RuntimeAcceptedPositionExecutionStatus parked =
|
|
drive.TryExecuteAcceptedPortalArrival(destination, portal);
|
|
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.DeferredCell, parked);
|
|
Assert.Empty(gameActions);
|
|
Assert.Equal(1, drive.PendingCount);
|
|
|
|
drive.Advance();
|
|
Assert.Equal(1, drive.PendingCount);
|
|
|
|
CommitLandblockCollision(runtime, deferredLandblock);
|
|
DrainPlacementFifo(runtime);
|
|
drive.Advance();
|
|
|
|
Assert.Equal(0, drive.PendingCount);
|
|
// Zero offset, not +192/+192: the accepted-teleport merge above
|
|
// already rebased the world frame onto deferredLandblock (#283),
|
|
// so the placement resolves in a frame ALREADY centered on the
|
|
// destination. Asserting the old cross-landblock delta here would
|
|
// be asserting a stale frame the merge already retired.
|
|
Assert.Equal(deferredPosition, controller.Position);
|
|
|
|
// The committed portal placement sends its ONE outbound movement
|
|
// event exactly like PortalCommitted_* asserts (D-T3's
|
|
// PlayerTeleported port; never an AutonomousPosition -
|
|
// SendPositionImmediately is always false for the portal route).
|
|
Assert.Single(gameActions);
|
|
|
|
// No re-issue path exists for a portal pending (trap T7): further
|
|
// pumps are pure no-ops, never a second commit or a stray ack.
|
|
drive.Advance();
|
|
drive.Advance();
|
|
Assert.Single(gameActions);
|
|
ConvergePortalHost(runtime, portal.RevealGeneration, portal.Projection);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A2/D-T2.4 review fix (2026-08-05): the wake path must re-validate
|
|
/// the portal authority before committing, so a park that resolves
|
|
/// AFTER something has made the authority stale (transit ended or was
|
|
/// superseded) does not run the reconcile/ack suffix — architecture
|
|
/// review A2's FIFO-wedge shape ("a Place receipt whose Token.Portal
|
|
/// still names the ended reveal"). The stub predicate below always
|
|
/// reports stale, standing in for that condition without needing to
|
|
/// actually end the transit mid-park.
|
|
/// </summary>
|
|
[Fact]
|
|
public void PortalDeferredCell_WakeAbandonsInsteadOfReconcilingWhenAuthorityWentStale()
|
|
{
|
|
using StartedRuntime started = StartRuntime();
|
|
GameRuntime runtime = started.Runtime;
|
|
(RuntimeEntityRecord record, PlayerMovementController controller) =
|
|
EnterLocalPlayer(runtime);
|
|
RuntimeAcceptedPositionDriveController drive =
|
|
CreateAcceptedPositionDrive(
|
|
runtime,
|
|
out List<byte[]> gameActions,
|
|
isPortalAuthorityCurrent: static _ => false);
|
|
|
|
const uint deferredLandblock = 0x02020000u;
|
|
var deferredPosition = new Vector3(10f, 10f, SpawnHeight);
|
|
const ushort teleportSequence = 9;
|
|
WorldSession.EntityPositionUpdate destinationUpdate = PortalDestinationUpdate(
|
|
deferredPosition, deferredLandblock | 0x0001u, teleportSequence);
|
|
MergeAccepted(runtime, controller, destinationUpdate);
|
|
(RuntimePortalPlacementAuthority portal, RuntimeTeleportDestination destination) =
|
|
BeginPortal(runtime, deferredLandblock | 0x0001u, teleportSequence, destinationUpdate);
|
|
// B8/A8 review fix: guarantees ConvergePortalHost runs even if an
|
|
// assertion below fails first - see PortalHostConvergenceGuard's doc.
|
|
using var portalHostGuard = new PortalHostConvergenceGuard(
|
|
runtime, portal.RevealGeneration, portal.Projection);
|
|
|
|
RuntimeAcceptedPositionExecutionStatus parked =
|
|
drive.TryExecuteAcceptedPortalArrival(destination, portal);
|
|
Assert.Equal(RuntimeAcceptedPositionExecutionStatus.DeferredCell, parked);
|
|
Assert.Equal(1, drive.PendingCount);
|
|
|
|
CommitLandblockCollision(runtime, deferredLandblock);
|
|
DrainPlacementFifo(runtime);
|
|
drive.Advance();
|
|
|
|
// The park resolved (PendingCount converges to zero either way —
|
|
// the wake always retires its retained operation) but because the
|
|
// stub reports the authority stale, NO reconcile/ack ran: zero
|
|
// outbound movement events, autorun untouched. The underlying body
|
|
// commit (RetryDeferred) is a SEPARATE, asynchronous mechanism this
|
|
// class cannot prevent (see the class doc on
|
|
// IsPortalAuthorityCurrent) — it still lands.
|
|
Assert.Equal(0, drive.PendingCount);
|
|
Assert.Empty(gameActions);
|
|
Assert.Equal(deferredPosition, controller.Position);
|
|
|
|
ConvergePortalHost(runtime, portal.RevealGeneration, portal.Projection);
|
|
AssertConverged(runtime);
|
|
}
|
|
|
|
private static WorldSession.EntityPositionUpdate PortalDestinationUpdate(
|
|
Vector3 position,
|
|
uint landblockId,
|
|
ushort teleportSequence,
|
|
ushort positionSequence = 2) =>
|
|
new(
|
|
PlayerGuid,
|
|
new CreateObject.ServerPosition(
|
|
landblockId,
|
|
position.X,
|
|
position.Y,
|
|
position.Z,
|
|
1f,
|
|
0f,
|
|
0f,
|
|
0f),
|
|
Velocity: null,
|
|
PlacementId: null,
|
|
IsGrounded: true,
|
|
InstanceSequence: 1,
|
|
PositionSequence: positionSequence,
|
|
TeleportSequence: teleportSequence,
|
|
ForcePositionSequence: 0);
|
|
|
|
/// <summary>
|
|
/// Mirrors <see cref="RuntimePlacementPresentationSinkTests.Fixture.BeginPortal"/>'s
|
|
/// shape against <see cref="GameRuntime.TransitOwner"/> directly - F751 →
|
|
/// offer → begin reveal → register the host token exactly like the
|
|
/// graphical/headless producers (D-T1).
|
|
/// </summary>
|
|
private static (RuntimePortalPlacementAuthority Portal, RuntimeTeleportDestination Destination)
|
|
BeginPortal(
|
|
GameRuntime runtime,
|
|
uint destinationCell,
|
|
ushort teleportSequence,
|
|
in WorldSession.EntityPositionUpdate destinationUpdate)
|
|
{
|
|
RuntimeWorldTransitState transit = runtime.TransitOwner;
|
|
Assert.True(transit.TryQueueTeleportStart(teleportSequence));
|
|
Assert.True(transit.ActivateQueuedTeleport());
|
|
var destination = new RuntimeTeleportDestination(
|
|
PlayerGuid,
|
|
InstanceSequence: 1,
|
|
PositionSequence: destinationUpdate.PositionSequence,
|
|
TeleportSequence: teleportSequence,
|
|
ForcePositionSequence: 0,
|
|
new Position(
|
|
destinationCell,
|
|
new Vector3(
|
|
destinationUpdate.Position.PositionX,
|
|
destinationUpdate.Position.PositionY,
|
|
destinationUpdate.Position.PositionZ),
|
|
Quaternion.Identity));
|
|
Assert.True(transit.OfferTeleportDestination(
|
|
destination,
|
|
teleportTimestampAdvanced: true));
|
|
Assert.True(transit.TryBeginPortalReveal(
|
|
teleportSequence,
|
|
destinationCell,
|
|
out long generation));
|
|
Assert.True(transit.TryRegisterHostProjection(
|
|
generation,
|
|
destinationCell,
|
|
out RuntimeWorldHostProjectionToken host));
|
|
return (
|
|
new RuntimePortalPlacementAuthority(true, generation, teleportSequence, host),
|
|
destination);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test-only host-side convergence: cancels the reveal generation and
|
|
/// drains the 4-stage host acknowledgement suffix
|
|
/// (<see cref="RuntimeWorldTransitState.RequireTerminalHostProjection"/>'s
|
|
/// SimulationReleaseProjected + DestinationReservationReleased +
|
|
/// TerminalProjected) so <see cref="RuntimeWorldTransitState.ResetSession"/>
|
|
/// does not throw during <c>StartedRuntime.Dispose()</c> teardown. A REAL
|
|
/// host (graphical/headless) always runs this suffix itself
|
|
/// (<c>LocalPlayerTeleportController.ResetTransit</c> /
|
|
/// <c>RuntimeLiveEntitySessionController.TryCompletePortal</c>'s
|
|
/// <c>Complete</c> path); this fixture stands in for that host exactly
|
|
/// like <c>DrainPlacementFifo</c> stands in for the placement
|
|
/// subscription.
|
|
/// </summary>
|
|
private static void ConvergePortalHost(
|
|
GameRuntime runtime,
|
|
long generation,
|
|
RuntimeWorldHostProjectionToken projection)
|
|
{
|
|
RuntimeWorldTransitState transit = runtime.TransitOwner;
|
|
if (!transit.Snapshot.Cancelled && !transit.Snapshot.Completed)
|
|
transit.Cancel(generation);
|
|
transit.AcknowledgeHostProjection(new RuntimeWorldHostAcknowledgement(
|
|
projection, RuntimeWorldHostAcknowledgementStage.SimulationReleaseProjected));
|
|
transit.AcknowledgeHostProjection(new RuntimeWorldHostAcknowledgement(
|
|
projection, RuntimeWorldHostAcknowledgementStage.DestinationReservationReleased));
|
|
transit.AcknowledgeHostProjection(new RuntimeWorldHostAcknowledgement(
|
|
projection, RuntimeWorldHostAcknowledgementStage.TerminalProjected));
|
|
transit.EndTeleport();
|
|
}
|
|
|
|
/// <summary>
|
|
/// B8/A8 review fix (2026-08-05): a plain trailing-statement call to
|
|
/// <see cref="ConvergePortalHost"/> is masked whenever an assertion
|
|
/// EARLIER in the same test body fails first - the real failure never
|
|
/// reaches xUnit because <c>StartedRuntime.Dispose()</c>'s own
|
|
/// <c>RuntimeWorldTransitState.ResetSession</c> throws a SECOND,
|
|
/// unrelated-looking "hosts=1, pending=1" exception while unwinding,
|
|
/// which is the one that actually surfaces. A <c>using var</c> local of
|
|
/// this guard, declared immediately after <see cref="BeginPortal"/>
|
|
/// returns, disposes on EVERY exit path (normal return AND exception
|
|
/// unwind) via C#'s own <c>using</c> semantics - equivalent to a
|
|
/// try/finally wrapping the rest of the method without the nesting.
|
|
/// <see cref="ConvergePortalHost"/> is idempotent against an already-
|
|
/// converged host (every step it performs is a no-op past the first
|
|
/// successful run), so a test that ALSO calls it explicitly on its own
|
|
/// success path is safe to leave as-is; this guard exists purely to
|
|
/// guarantee the call still happens when that explicit call is never
|
|
/// reached.
|
|
/// </summary>
|
|
private readonly struct PortalHostConvergenceGuard : IDisposable
|
|
{
|
|
private readonly GameRuntime _runtime;
|
|
private readonly long _generation;
|
|
private readonly RuntimeWorldHostProjectionToken _projection;
|
|
|
|
public PortalHostConvergenceGuard(
|
|
GameRuntime runtime,
|
|
long generation,
|
|
RuntimeWorldHostProjectionToken projection)
|
|
{
|
|
_runtime = runtime;
|
|
_generation = generation;
|
|
_projection = projection;
|
|
}
|
|
|
|
public void Dispose() =>
|
|
ConvergePortalHost(_runtime, _generation, _projection);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private static void AssertConverged(GameRuntime runtime)
|
|
{
|
|
RuntimeEntityObjectOwnershipSnapshot ownership =
|
|
runtime.EntityObjects.CaptureOwnership();
|
|
Assert.Equal(0, ownership.AcceptedPositionDrivePendingCount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs the SAME upstream merge production runs BEFORE this route
|
|
/// ever sees a Position
|
|
/// (<see cref="RuntimeEntityObjectLifetime.TryApplyPosition"/>, called by
|
|
/// <c>LiveEntityInboundAuthorityGate.TryAcceptPosition</c> in App and
|
|
/// directly in <c>RuntimeLiveEntitySessionController.OnPositionUpdated</c>
|
|
/// in headless) — this is what actually admits the disposition via the
|
|
/// REAL <c>PhysicsTimestampGate</c> and substitutes the controller's
|
|
/// current heading into <c>record.Snapshot</c> for a ForcePosition
|
|
/// (contract §1c), rather than hand-rolling a disposition/timestamps pair
|
|
/// the seam's caller could never actually observe.
|
|
/// </summary>
|
|
private static (PositionTimestampDisposition Disposition, AcceptedPhysicsTimestamps Timestamps)
|
|
MergeAccepted(
|
|
GameRuntime runtime,
|
|
PlayerMovementController controller,
|
|
in WorldSession.EntityPositionUpdate update)
|
|
{
|
|
Assert.True(runtime.EntityObjects.TryApplyPosition(
|
|
update,
|
|
isLocalPlayer: true,
|
|
forcePositionRotation: controller.BodyOrientation,
|
|
currentLocalVelocity: controller.BodyVelocity,
|
|
acknowledgeProjection: null,
|
|
out PositionTimestampDisposition disposition,
|
|
out _,
|
|
out AcceptedPhysicsTimestamps timestamps));
|
|
return (disposition, timestamps);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A wire Position whose FORCE_POSITION_TS strictly advances, so the REAL
|
|
/// <see cref="PhysicsTimestampGate.TryAcceptPositionEvent"/> admits it as
|
|
/// <see cref="PositionTimestampDisposition.ForcePosition"/> (retail
|
|
/// <c>SmartBox::HandleReceivedPosition</c> @0x00453FD0's FORCE_POSITION
|
|
/// branch: fresh FORCE_POSITION_TS with an exactly-equal TELEPORT_TS).
|
|
/// The spawn seeds FORCE_POSITION_TS 0 / POSITION_TS 1, so the defaults
|
|
/// are the FIRST such packet; a session's SECOND force must raise
|
|
/// <paramref name="forcePositionSequence"/> again.
|
|
/// </summary>
|
|
private static WorldSession.EntityPositionUpdate ForceUpdate(
|
|
Vector3 position,
|
|
uint landblockId = SpawnLandblock | 0x0001u,
|
|
ushort positionSequence = 2,
|
|
ushort forcePositionSequence = 1) =>
|
|
new(
|
|
PlayerGuid,
|
|
new CreateObject.ServerPosition(
|
|
landblockId,
|
|
position.X,
|
|
position.Y,
|
|
position.Z,
|
|
1f,
|
|
0f,
|
|
0f,
|
|
0f),
|
|
Velocity: null,
|
|
PlacementId: null,
|
|
IsGrounded: true,
|
|
InstanceSequence: 1,
|
|
PositionSequence: positionSequence,
|
|
TeleportSequence: 0,
|
|
ForcePositionSequence: forcePositionSequence);
|
|
|
|
/// <summary>
|
|
/// An ORDINARY server position echo: FORCE_POSITION_TS does NOT advance,
|
|
/// POSITION_TS strictly does, so the same real gate admits it as
|
|
/// <see cref="PositionTimestampDisposition.Apply"/>. Neither host ever
|
|
/// dispatches this disposition to the drive — it merges (advancing
|
|
/// <c>PositionAuthorityVersion</c> and Forgetting any in-flight placement)
|
|
/// entirely behind the drive's back, which is exactly what makes it the
|
|
/// N2 case.
|
|
/// </summary>
|
|
private static WorldSession.EntityPositionUpdate OrdinaryUpdate(
|
|
Vector3 position,
|
|
ushort positionSequence,
|
|
ushort forcePositionSequence = 1,
|
|
uint landblockId = SpawnLandblock | 0x0001u) =>
|
|
ForceUpdate(
|
|
position,
|
|
landblockId,
|
|
positionSequence,
|
|
forcePositionSequence);
|
|
|
|
private static AcceptedPhysicsTimestamps Timestamps(ushort teleport) =>
|
|
new(
|
|
Instance: 1,
|
|
ServerControlledMove: 1,
|
|
Teleport: teleport,
|
|
ForcePosition: 1,
|
|
TeleportAdvanced: false,
|
|
PreviousTeleport: teleport);
|
|
|
|
/// <summary>
|
|
/// Spawns the local player, drives its initial-Create residence to
|
|
/// completion, and returns the resulting canonical record + live
|
|
/// controller — the fixture every test above needs before a
|
|
/// ForcePosition can be route-2-eligible at all.
|
|
/// </summary>
|
|
private static (RuntimeEntityRecord Record, PlayerMovementController Controller)
|
|
EnterLocalPlayer(GameRuntime runtime)
|
|
{
|
|
runtime.PlayerIdentity.ServerGuid = PlayerGuid;
|
|
CommitLandblockCollision(runtime, SpawnLandblock);
|
|
RuntimeFirstEntryDriveController firstEntry = CreateFirstEntryDrive(runtime);
|
|
using var session = new WorldSession(
|
|
new IPEndPoint(IPAddress.Loopback, 9000),
|
|
new FixtureTransport());
|
|
var sessionController = new RuntimeLiveEntitySessionController(
|
|
runtime,
|
|
session,
|
|
worldProjection: new FixtureWorldProjection(firstEntry));
|
|
LiveEntitySessionSink sink = sessionController.CreateSink();
|
|
|
|
sink.Spawned(Spawn(PlayerGuid));
|
|
DrainFirstEntry(runtime, firstEntry);
|
|
|
|
RuntimeEntityRecord record = Assert.IsType<RuntimeEntityRecord>(
|
|
GetActive(runtime, PlayerGuid));
|
|
PlayerMovementController controller = Assert.IsType<PlayerMovementController>(
|
|
runtime.MovementOwner.Controller);
|
|
return (record, controller);
|
|
}
|
|
|
|
private static RuntimeEntityRecord GetActive(GameRuntime runtime, uint guid)
|
|
{
|
|
Assert.True(runtime.EntityObjects.Entities.TryGetActive(
|
|
guid, out RuntimeEntityRecord record));
|
|
return record;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructs the drive controller with a LIVE (deliberately never
|
|
/// disposed within this factory — it must outlive the whole test)
|
|
/// fixture <see cref="WorldSession"/> whose captured outbound game
|
|
/// actions the caller can assert against.
|
|
/// </summary>
|
|
private static RuntimeAcceptedPositionDriveController CreateAcceptedPositionDrive(
|
|
GameRuntime runtime,
|
|
out List<byte[]> gameActions,
|
|
Func<RuntimePortalPlacementAuthority, bool>? isPortalAuthorityCurrent = null)
|
|
{
|
|
var captured = new List<byte[]>();
|
|
gameActions = captured;
|
|
var liveSession = new WorldSession(
|
|
new IPEndPoint(IPAddress.Loopback, 9001),
|
|
new FixtureTransport())
|
|
{
|
|
GameActionCapture = body => captured.Add(body),
|
|
};
|
|
return new RuntimeAcceptedPositionDriveController(
|
|
runtime.EntityObjects,
|
|
runtime.Clock,
|
|
new UnusedCollisionSource(),
|
|
new LocalPlayerOutboundController((_, _, _, _, _, _) => { }),
|
|
() => runtime.Generation,
|
|
() => runtime.PlayerIdentity.ServerGuid,
|
|
() => runtime.MovementOwner.Controller,
|
|
() => runtime.CharacterOwner.UsePositionFromServer,
|
|
() => liveSession,
|
|
// C4 route 3: the portal arm's PlayerTeleported port needs the
|
|
// autorun latch owner. Unused on the force arm this factory has
|
|
// always served, so every existing route-2 test is unaffected.
|
|
() => runtime.MovementOwner,
|
|
// A2/D-T2.4 review fix (2026-08-05): defaults to null (existing
|
|
// callers unaffected - every retained portal pending is treated
|
|
// as current, today's unconditional behaviour).
|
|
isPortalAuthorityCurrent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A flat landblock whose terrain surface sits exactly at
|
|
/// <see cref="SpawnHeight"/> — every fixture position in this file uses
|
|
/// that Z so <c>CommitCanonical</c>'s contact resolve genuinely finds
|
|
/// ground (retail <c>CommandInterpreter::SendPositionEvent</c>'s own
|
|
/// admission gate requires Contact+OnWalkable —
|
|
/// <c>PlayerMovementController.CanSendPositionEvent</c> — so an airborne
|
|
/// fixture body would silently suppress every ack this suite asserts).
|
|
/// </summary>
|
|
private static void CommitLandblockCollision(
|
|
GameRuntime runtime,
|
|
uint landblockId,
|
|
float worldOffsetX = 0f)
|
|
{
|
|
// Mirrors HeadlessSessionHostTests.AddFlatLandblock's exact
|
|
// proven-working shape (every heightmap byte and every table entry
|
|
// participate) rather than a sparse table — a resolve near a
|
|
// landblock edge samples neighbouring grid entries too.
|
|
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;
|
|
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
|
landblockId, 1UL);
|
|
runtime.EntityObjects.Physics.Engine.AddLandblock(
|
|
landblockId,
|
|
new TerrainSurface(heights, heightTable),
|
|
Array.Empty<CellSurface>(),
|
|
Array.Empty<PortalPlane>(),
|
|
worldOffsetX,
|
|
worldOffsetY: 0f);
|
|
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
|
landblockId, 1UL, ready: true);
|
|
runtime.EntityObjects.Physics.ObserveLocalWorldFrame(
|
|
landblockId | 0x0001u,
|
|
teleportAdvanced: false);
|
|
}
|
|
|
|
private static RuntimeFirstEntryDriveController CreateFirstEntryDrive(
|
|
GameRuntime runtime) =>
|
|
new(
|
|
runtime.EntityObjects,
|
|
runtime.Clock,
|
|
new UnusedCollisionSource(),
|
|
() => PlayerMovementConstructionOptions.Fallback,
|
|
static _ => new RuntimeLocalPlayerPhysicsActivationPreparation(
|
|
Radius: 0.48f,
|
|
Height: 1.835f,
|
|
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
|
|
|
|
private static void DrainFirstEntry(
|
|
GameRuntime runtime,
|
|
RuntimeFirstEntryDriveController drive)
|
|
{
|
|
for (int attempt = 0; attempt < 8 && drive.PendingCount != 0; attempt++)
|
|
{
|
|
drive.DriveAll();
|
|
DrainPlacementFifo(runtime);
|
|
}
|
|
Assert.Equal(0, drive.PendingCount);
|
|
}
|
|
|
|
private static void DrainPlacementFifo(GameRuntime runtime)
|
|
{
|
|
while (runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
|
|
out RuntimePlacementProjectionSnapshot head))
|
|
{
|
|
if (!runtime.EntityObjects.Physics.SetPosition
|
|
.AcknowledgeProjection(head.Token))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static WorldSession.EntitySpawn Spawn(uint guid)
|
|
{
|
|
var position = new CreateObject.ServerPosition(
|
|
SpawnLandblock | 0x0001u,
|
|
10f,
|
|
10f,
|
|
5f,
|
|
1f,
|
|
0f,
|
|
0f,
|
|
0f);
|
|
var timestamps = new PhysicsTimestamps(
|
|
Position: 1,
|
|
Movement: 1,
|
|
State: 1,
|
|
Vector: 1,
|
|
Teleport: 0,
|
|
ServerControlledMove: 1,
|
|
ForcePosition: 0,
|
|
ObjDesc: 1,
|
|
Instance: 1);
|
|
var physics = new PhysicsSpawnData(
|
|
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
|
Position: position,
|
|
Movement: null,
|
|
AnimationFrame: null,
|
|
SetupTableId: null,
|
|
MotionTableId: null,
|
|
SoundTableId: null,
|
|
PhysicsScriptTableId: null,
|
|
Parent: null,
|
|
Children: null,
|
|
Scale: null,
|
|
Friction: null,
|
|
Elasticity: null,
|
|
Translucency: null,
|
|
Velocity: null,
|
|
Acceleration: null,
|
|
AngularVelocity: null,
|
|
DefaultScriptType: null,
|
|
DefaultScriptIntensity: null,
|
|
Timestamps: timestamps);
|
|
return new WorldSession.EntitySpawn(
|
|
guid,
|
|
position,
|
|
null,
|
|
[],
|
|
[],
|
|
[],
|
|
null,
|
|
null,
|
|
"direct entity",
|
|
null,
|
|
null,
|
|
null,
|
|
PhysicsState: physics.RawState,
|
|
InstanceSequence: 1,
|
|
MovementSequence: 1,
|
|
ServerControlSequence: 1,
|
|
PositionSequence: 1,
|
|
Physics: physics);
|
|
}
|
|
|
|
private sealed class UnusedCollisionSource
|
|
: AcDream.Content.IPreparedCollisionSource
|
|
{
|
|
public AcDream.Content.PreparedAssetPresence ProbeCollision(
|
|
AcDream.Content.Pak.PakAssetType type,
|
|
uint sourceFileId) =>
|
|
AcDream.Content.PreparedAssetPresence.Available;
|
|
|
|
public AcDream.Content.PreparedCollisionReadResult<
|
|
FlatSetupCollision> ReadSetupCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
AcDream.Content.PreparedCollisionReadResult<
|
|
FlatSetupCollision>.Missing;
|
|
|
|
public AcDream.Content.PreparedCollisionReadResult<
|
|
FlatGfxObjCollisionAsset> ReadGfxObjCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException();
|
|
|
|
public AcDream.Content.PreparedCollisionReadResult<
|
|
FlatCellStructureCollisionAsset> ReadCellStructureCollision(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException();
|
|
|
|
public AcDream.Content.PreparedCollisionReadResult<
|
|
FlatEnvCellTopology> ReadEnvCellTopology(
|
|
uint sourceFileId,
|
|
CancellationToken cancellationToken = default) =>
|
|
throw new NotSupportedException();
|
|
|
|
public AcDream.Content.PreparedCollisionSourceStats CollisionStats =>
|
|
default;
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class FixtureTransport : IWorldSessionTransport
|
|
{
|
|
public void Send(ReadOnlySpan<byte> datagram)
|
|
{
|
|
}
|
|
|
|
public void Send(
|
|
IPEndPoint remote,
|
|
ReadOnlySpan<byte> datagram)
|
|
{
|
|
}
|
|
|
|
public int Receive(
|
|
Span<byte> destination,
|
|
TimeSpan timeout,
|
|
out IPEndPoint? from)
|
|
{
|
|
from = null;
|
|
return -1;
|
|
}
|
|
|
|
public ValueTask<NetReceiveResult> ReceiveAsync(
|
|
Memory<byte> destination,
|
|
CancellationToken cancellationToken) =>
|
|
ValueTask.FromException<NetReceiveResult>(
|
|
new OperationCanceledException(cancellationToken));
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class FixtureWorldProjection : IRuntimeDirectWorldProjection
|
|
{
|
|
private readonly RuntimeFirstEntryDriveController _firstEntry;
|
|
|
|
internal FixtureWorldProjection(RuntimeFirstEntryDriveController firstEntry) =>
|
|
_firstEntry = firstEntry;
|
|
|
|
public void ProjectSpawn(RuntimeEntityRecord record, bool isLocalPlayer) =>
|
|
_firstEntry.DriveAll();
|
|
|
|
public void ProjectPosition(
|
|
RuntimeEntityRecord record,
|
|
bool isLocalPlayer,
|
|
PositionTimestampDisposition disposition)
|
|
{
|
|
}
|
|
|
|
public void CenterOnAcceptedForcePosition(RuntimeEntityRecord record)
|
|
{
|
|
}
|
|
|
|
public void BeginTeleport()
|
|
{
|
|
}
|
|
|
|
public RuntimeDestinationReadiness PrepareDestination(
|
|
long revealGeneration,
|
|
RuntimeTeleportDestination destination,
|
|
RuntimeWorldHostProjectionToken portal) =>
|
|
new(
|
|
revealGeneration,
|
|
destination.CellId,
|
|
IsIndoor: false,
|
|
IsUnhydratable: false,
|
|
RequiredRenderRadius: 1,
|
|
IsRenderNeighborhoodReady: true,
|
|
AreCompositeTexturesReady: true,
|
|
IsCollisionReady: true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C3c: initial-residence admission requires a live session generation
|
|
/// (RuntimeInitialCreateResidenceState.CanAcceptCreate), so this test
|
|
/// starts one through the same fixture-session shape
|
|
/// DirectGameRuntimeCommandAdapterTests / RuntimeLiveEntitySessionControllerTests use.
|
|
/// </summary>
|
|
private sealed class StartedRuntime : IDisposable
|
|
{
|
|
internal required GameRuntime Runtime { get; init; }
|
|
internal required LiveSessionHost Live { get; init; }
|
|
|
|
public void Dispose()
|
|
{
|
|
_ = Live.Stop(Runtime.Generation);
|
|
Runtime.Dispose();
|
|
}
|
|
}
|
|
|
|
private static StartedRuntime StartRuntime()
|
|
{
|
|
var operations = new FixtureGameplayOperations();
|
|
var sessionOperations = new FixtureSessionOperations();
|
|
var runtime = new GameRuntime(new GameRuntimeDependencies(
|
|
operations,
|
|
operations,
|
|
operations,
|
|
operations,
|
|
SessionOperations: sessionOperations));
|
|
operations.Bind(runtime);
|
|
var resetHost = new FixtureResetHost();
|
|
var options = new LiveSessionConnectOptions(
|
|
true,
|
|
"127.0.0.1",
|
|
9000,
|
|
"account",
|
|
"password");
|
|
var live = new LiveSessionHost(
|
|
runtime.Session,
|
|
new LiveSessionHostBindings(
|
|
new LiveSessionRoutingFactories(
|
|
_ => new FixtureEventRoute(),
|
|
_ => new FixtureCommandRoute()),
|
|
generation => runtime.ResetGeneration(generation, resetHost),
|
|
new LiveSessionSelectionBindings(
|
|
id => runtime.PlayerIdentity.ServerGuid = id,
|
|
_ => { },
|
|
runtime.CommunicationOwner.Chat.SetLocalPlayerGuid,
|
|
_ => { },
|
|
_ => { },
|
|
runtime.ActionOwner.Combat.Clear),
|
|
new LiveSessionEnteredWorldBindings(
|
|
_ => { },
|
|
() => { },
|
|
() => { },
|
|
_ => { },
|
|
() => { }),
|
|
(_, _, _) => { },
|
|
() => { }),
|
|
options);
|
|
LiveSessionStartResult startResult = live.Start(options);
|
|
Assert.Equal(LiveSessionStartStatus.Connected, startResult.Status);
|
|
Assert.NotEqual(0UL, runtime.Generation.Value);
|
|
return new StartedRuntime { Runtime = runtime, Live = live };
|
|
}
|
|
|
|
private sealed class FixtureSessionOperations : ILiveSessionOperations
|
|
{
|
|
public IPEndPoint ResolveEndpoint(string host, int port) =>
|
|
new(IPAddress.Loopback, port);
|
|
|
|
public WorldSession CreateSession(IPEndPoint endpoint) =>
|
|
new(endpoint, new FixtureTransport());
|
|
|
|
public void Connect(WorldSession session, string user, string password)
|
|
{
|
|
}
|
|
|
|
public CharacterList.Parsed GetCharacters(WorldSession session) =>
|
|
new(
|
|
0u,
|
|
[new CharacterList.Character(PlayerGuid, "Direct", 0u)],
|
|
[],
|
|
11,
|
|
"account",
|
|
true,
|
|
true);
|
|
|
|
public void EnterWorld(WorldSession session, int activeCharacterIndex)
|
|
{
|
|
}
|
|
|
|
public void Tick(WorldSession session)
|
|
{
|
|
}
|
|
|
|
public void DisposeSession(WorldSession session) =>
|
|
session.Dispose();
|
|
}
|
|
|
|
private sealed class FixtureEventRoute : ILiveSessionEventRouting
|
|
{
|
|
public void Attach()
|
|
{
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class FixtureCommandRoute : ILiveSessionCommandRouting
|
|
{
|
|
public void Activate()
|
|
{
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class FixtureResetHost : IRuntimeGenerationResetHost
|
|
{
|
|
public void RetireEntityProjection(RuntimeEntityRecord entity)
|
|
{
|
|
}
|
|
|
|
public void DrainEntityProjectionBoundary()
|
|
{
|
|
}
|
|
|
|
public void CompleteEntityProjectionRetirement()
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class FixtureGameplayOperations
|
|
: IRuntimeCombatAttackOperations,
|
|
IRuntimeCombatTargetOperations,
|
|
IRuntimeCombatModeOperations,
|
|
IRuntimeSpellCastOperations
|
|
{
|
|
private GameRuntime? _runtime;
|
|
|
|
public void Bind(GameRuntime runtime) => _runtime = runtime;
|
|
public bool CanStartAttack() => false;
|
|
public void PrepareAttackRequest()
|
|
{
|
|
}
|
|
|
|
public bool SendAttack(AttackHeight height, float power) => false;
|
|
public void SendCancelAttack()
|
|
{
|
|
}
|
|
|
|
public bool IsDualWield => false;
|
|
public bool PlayerReadyForAttack => false;
|
|
public bool AutoRepeatAttack => false;
|
|
public bool AutoTarget => false;
|
|
public uint? SelectClosestTarget() => null;
|
|
public bool IsInWorld => _runtime?.Session.IsInWorld == true;
|
|
public IReadOnlyList<ClientObject> GetOrderedEquipment() => [];
|
|
public void NotifyExplicitCombatModeRequest()
|
|
{
|
|
}
|
|
|
|
public void SendChangeCombatMode(CombatMode mode)
|
|
{
|
|
}
|
|
|
|
public uint LocalPlayerId =>
|
|
_runtime?.PlayerIdentity.ServerGuid ?? 0u;
|
|
public bool CanSend => false;
|
|
public bool HasRequiredComponents(uint spellId) => false;
|
|
|
|
public bool IsTargetCompatible(
|
|
uint targetId,
|
|
SpellMetadata spell,
|
|
bool showMessage) => false;
|
|
|
|
public void StopCompletely()
|
|
{
|
|
}
|
|
|
|
public void SendUntargeted(uint spellId)
|
|
{
|
|
}
|
|
|
|
public void SendTargeted(uint targetId, uint spellId)
|
|
{
|
|
}
|
|
|
|
public void DisplayMessage(string message)
|
|
{
|
|
}
|
|
|
|
public void IncrementBusy()
|
|
{
|
|
}
|
|
}
|
|
}
|