fix(physics): C4 route 3 — portal placement authority (local player)
Removes a duplicate placement authority for local-player portal arrival. Portalling worked before this change and works after it — this is not a bug fix, EXCEPT that it found and fixed one dead-code production bug. THE PRODUCTION BUG: TryExecuteCanonicalPortalPlacement re-read the accepted destination at Place time, but TryBeginPortalReveal already consumes that slot at Aim time — so the arm was 100% dead code and every real portal Place refused with host-token-unavailable. Found only because we refused to accept 7 skipped tests instead of chasing the count to zero. RETAIL IS THE GENERIC PATH FOR THE THIRD ROUTE RUNNING: SmartBox::TeleportPlayer @0x00453910 = SetPositionSimple(dest, 1) with flags 0x1012, followed by PlayerPositionUpdated. BOTH INVERSIONS, WITH THEIR ANCHORS: unlike route 2, the leash IS armed here (ConstrainTo @0x0045418A) and velocity is zeroed (set_velocity @0x004541B4); unlike route 4b-3, the local teleport_hook runs AFTER placement (@0x004538AE). THE THREE-ROUND DEFECT CHAIN, HONESTLY: - Round 1 released the player at the pre-teleport position while the anim stream marched on — the contract wrongly assumed Place re-fires (process rule 1's third occurrence this campaign). - Round 2's fix inferred commit from a global PendingCount, which three non-committing paths also clear — making the SAME bug complete cleanly and silently. Strictly worse than round 1: round 1 at least tripped portal-complete-before-materialized. - Round 3 latches the commit where it actually happens (ReconcileAndAcknowledgePortal), keyed on reveal generation and teleport sequence, via TryConsumePortalCommit. Two of the three required regression tests landed and are sabotage-verified on both hosts (ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted / HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted). The third (force-arm-takes-the-slot) was judged unnecessary on review: with the inference gone, PendingCount is only a "don't ask yet" guard at both gates, so a force operation occupying or vacating the slot no longer changes an input the commit decision reads — the case collapses into what the landed test already discriminates. THE B2/P3 RESOLUTION: both round-2 reviews were right about different branches of the same synchronous call. RuntimePlacementProjectionSubscription .OnPlacement acknowledges the FIFO head only when TryApply returns true; a Place whose portal authority went stale (transit ended/superseded while parked) used to return false, wedging every later entity's placement receipt behind it forever. Both sinks (RuntimePlacementPresentationSink, HeadlessRuntimePlacementProjectionSink) now acknowledge-and-ignore a stale-authority Place instead of refusing it. The regression test (RuntimePlacementPresentationSinkTests .PortalPlace_StaleTransitHostOrSequenceIsAcknowledgedAndIgnored) had been asserting the old, wrong `false` behaviour; it now asserts and sabotage-verifies the fix. Also lands: AP-144 (register discipline — the portal movement-event send reuses the stricter UsePositionFromServer gate where retail's SendMovementEvent is the looser autonomy_level != 0 test, diverging only at level 1, currently unreachable), AP-145 + issue #318 (the local-player collision-shadow presentation write bypasses its own publisher's ShadowObjects write via a direct cache .Set(), self-healing only once dedup diverges — filed, not fixed, pending a composition test), AD-42 deleted (its last citation retired by the canonical portal arm), AD-2 updated (the wait-cue's trigger predicate now covers a second cause), and two documentation corrections: the enter_world misattribution (both call sites are in SmartBox::HandleCreateObject, only one in the player branch — portal arrival is TeleportPlayer, not enter_world) and the stale "local player never reaches this path" comment on the generic-remote-render-pose write. Suite: 11,090 passed / 4 skipped / 0 failed. No new skips, nothing weakened. STILL OWED: the connected two-client gate, with ACDREAM_PROBE_LOCAL_TELEPORT=1, scored only if [local-tp] lines actually appear in the capture — and explicitly NOT scored as covering issue #318 (no composition test yet asserts PhysicsEngine.ShadowObjects directly). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
cd3129e9d6
commit
e0f96a55bf
24 changed files with 5261 additions and 243 deletions
|
|
@ -383,10 +383,20 @@ public sealed class HeadlessSessionHostTests
|
|||
record.Snapshot,
|
||||
replaceGeneration: false));
|
||||
var collision = new FixtureCollisionNeighborhood();
|
||||
// R5/A7 review fix (2026-08-05): the drive controller is now wired
|
||||
// (was omitted in the first pass, leaving the canonical portal arm
|
||||
// a no-op by construction here and dual-host parity with zero
|
||||
// coverage). Mirrors production composition
|
||||
// (HeadlessSessionHost.cs's own construction order): the SAME
|
||||
// RuntimeAcceptedPositionDriveController drives both hosts through
|
||||
// the identical TryExecuteAcceptedPortalArrival entry point.
|
||||
RuntimeAcceptedPositionDriveController acceptedPositionDrive =
|
||||
CreateAcceptedPositionDrive(runtime);
|
||||
var projection = new HeadlessSessionWorldProjection(
|
||||
runtime,
|
||||
collision,
|
||||
firstEntry);
|
||||
firstEntry,
|
||||
acceptedPositionDrive);
|
||||
|
||||
projection.ProjectSpawn(record, isLocalPlayer: true);
|
||||
PlayerMovementController controller =
|
||||
|
|
@ -410,6 +420,15 @@ public sealed class HeadlessSessionHostTests
|
|||
projection.BeginTeleport();
|
||||
Assert.Equal(PlayerState.PortalSpace, controller.State);
|
||||
|
||||
// R5/A7: the destination cell 0xA9B40001 is in the SAME landblock
|
||||
// (0xA9B40000) whose collision generation the test already
|
||||
// committed above, so the canonical portal arm resolves
|
||||
// Committed synchronously - no DeferredCell park needed to exercise
|
||||
// the real headless placement path. A1's headless retry loop
|
||||
// (RuntimeLiveEntitySessionController.PumpPortalCompletion) is
|
||||
// covered separately by
|
||||
// HeadlessPortalDeferredCellCommitsOnPumpAfterCollisionGenerationWake
|
||||
// below.
|
||||
RuntimeDestinationReadiness readiness =
|
||||
projection.PrepareDestination(
|
||||
revealGeneration: 7,
|
||||
|
|
@ -422,13 +441,393 @@ public sealed class HeadlessSessionHostTests
|
|||
new Position(
|
||||
0xA9B40001u,
|
||||
new Vector3(96f, 97f, 50f),
|
||||
Quaternion.Identity)));
|
||||
Quaternion.Identity)),
|
||||
// R5/A7: a real, valid token - `default` was fine for the
|
||||
// old no-op arm but RuntimePortalPlacementAuthority.IsValid
|
||||
// now genuinely gates TryExecuteAcceptedPortalArrival on it.
|
||||
new RuntimeWorldHostProjectionToken(7, 0xA9B40001u));
|
||||
|
||||
Assert.True(readiness.IsCollisionReady);
|
||||
Assert.False(readiness.IsUnhydratable);
|
||||
Assert.Equal(PlayerState.InWorld, controller.State);
|
||||
Assert.Equal(3, collision.CenterCount);
|
||||
Assert.Equal(2, collision.CenterCount);
|
||||
Assert.Equal(0xA9B40001u, collision.LastCell);
|
||||
// A4/dual-host parity: the canonical placement actually committed -
|
||||
// the body moved to the destination Position, not just the
|
||||
// collision-neighborhood bookkeeping that CenterCount/LastCell
|
||||
// alone would have proven even with the earlier no-op arm. Z
|
||||
// settles 0.005 above the wire value (the foot sphere's bottom
|
||||
// sits at origin + 0.475 - 0.48, LoadedSetupCollisionSource's own
|
||||
// doc comment, ISSUES.md #285) - X/Y are exact, Z is asserted
|
||||
// within that settle tolerance.
|
||||
Assert.Equal(96f, controller.Position.X);
|
||||
Assert.Equal(97f, controller.Position.Y);
|
||||
Assert.Equal(50f, controller.Position.Z, 0.01f);
|
||||
// The resolved outdoor sub-cell index is derived from X/Y within
|
||||
// the landblock (not the wire placeholder 0xA9B40001), same as the
|
||||
// FIRST ProjectPosition assertion above (":406-409") only checks
|
||||
// landblock+indoor-vs-outdoor, not the exact sub-cell.
|
||||
Assert.Equal(0xA9B40000u, controller.CellId & 0xFFFF0000u);
|
||||
Assert.True((controller.CellId & 0xFFFFu) < 0x0100u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A1/R5/A7 review fix (2026-08-05): headless has no per-frame anim
|
||||
/// sequencer the way the graphical host does, so its OWN equivalent of
|
||||
/// A1's "hold until committed" mechanism is
|
||||
/// <c>HeadlessSessionWorldProjection.PrepareDestination</c>'s
|
||||
/// <c>_awaitingPortalWake</c> polling. This proves it end to end: a
|
||||
/// destination in a landblock whose collision generation is NOT yet
|
||||
/// committed parks (<c>IsCollisionReady: false</c>, body unmoved, no
|
||||
/// throw — DeferredCell is a normal headless outcome per
|
||||
/// <c>PrepareDestination</c>'s own doc), and once the destination
|
||||
/// landblock's collision generation commits, the SAME park resolves on
|
||||
/// a later attempt WITHOUT a second concurrent Begin (Runtime's own
|
||||
/// Begin would refuse that with Contention if this class re-attempted
|
||||
/// blindly instead of polling <c>PendingCount</c>).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake()
|
||||
{
|
||||
var operations = new FixtureSessionOperations();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
GameRuntime runtime = host.Runtime;
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
host.Start().Status);
|
||||
const uint player = 0x50000009u;
|
||||
const uint destinationLandblock = 0xAAB40000u;
|
||||
runtime.PlayerIdentity.ServerGuid = player;
|
||||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
||||
0xA9B40000u, 1UL);
|
||||
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
||||
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
||||
0xA9B40000u, 1UL, ready: true);
|
||||
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
|
||||
CreateFirstEntryDrive(runtime);
|
||||
RuntimeEntityRecord record = runtime.EntityObjects
|
||||
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
|
||||
.Canonical!;
|
||||
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||
record,
|
||||
record.CreateIntegrationVersion,
|
||||
record.Snapshot,
|
||||
replaceGeneration: false));
|
||||
var collision = new FixtureCollisionNeighborhood();
|
||||
RuntimeAcceptedPositionDriveController acceptedPositionDrive =
|
||||
CreateAcceptedPositionDrive(runtime);
|
||||
var projection = new HeadlessSessionWorldProjection(
|
||||
runtime,
|
||||
collision,
|
||||
firstEntry,
|
||||
acceptedPositionDrive);
|
||||
|
||||
projection.ProjectSpawn(record, isLocalPlayer: true);
|
||||
PlayerMovementController controller =
|
||||
Assert.IsType<PlayerMovementController>(
|
||||
runtime.MovementOwner.Controller);
|
||||
controller.SetPosition(
|
||||
new Vector3(48f, 49f, 50f),
|
||||
0xA9B40001u);
|
||||
projection.ProjectPosition(
|
||||
record,
|
||||
isLocalPlayer: true,
|
||||
PositionTimestampDisposition.Apply);
|
||||
projection.BeginTeleport();
|
||||
|
||||
var destination = new RuntimeTeleportDestination(
|
||||
player,
|
||||
InstanceSequence: 1,
|
||||
PositionSequence: 2,
|
||||
TeleportSequence: 1,
|
||||
ForcePositionSequence: 0,
|
||||
new Position(
|
||||
destinationLandblock | 0x0001u,
|
||||
new Vector3(10f, 10f, 50f),
|
||||
Quaternion.Identity));
|
||||
var projectionToken = new RuntimeWorldHostProjectionToken(
|
||||
7, destinationLandblock | 0x0001u);
|
||||
|
||||
// Begin's own portal-vs-latest-cell gate (D-T5) requires the
|
||||
// destination's landblock to match the record's LATEST MERGED
|
||||
// Position, not just the transit's retained destination - mirror
|
||||
// what the real inbound Position handler already does before
|
||||
// TryCompletePortal ever runs (LiveEntityNetworkUpdateController's
|
||||
// App-side equivalent).
|
||||
Assert.True(runtime.EntityObjects.TryApplyPosition(
|
||||
new WorldSession.EntityPositionUpdate(
|
||||
player,
|
||||
new CreateObject.ServerPosition(
|
||||
destination.Position.ObjCellId,
|
||||
destination.Position.Frame.Origin.X,
|
||||
destination.Position.Frame.Origin.Y,
|
||||
destination.Position.Frame.Origin.Z,
|
||||
destination.Position.Frame.Orientation.W,
|
||||
destination.Position.Frame.Orientation.X,
|
||||
destination.Position.Frame.Orientation.Y,
|
||||
destination.Position.Frame.Orientation.Z),
|
||||
Velocity: null,
|
||||
PlacementId: null,
|
||||
IsGrounded: true,
|
||||
InstanceSequence: 1,
|
||||
PositionSequence: 3,
|
||||
TeleportSequence: destination.TeleportSequence,
|
||||
ForcePositionSequence: 0),
|
||||
isLocalPlayer: true,
|
||||
forcePositionRotation: Quaternion.Identity,
|
||||
currentLocalVelocity: Vector3.Zero,
|
||||
acknowledgeProjection: null,
|
||||
out _,
|
||||
out _,
|
||||
out _));
|
||||
|
||||
// First attempt: destinationLandblock's collision generation was
|
||||
// never begun/committed, so the canonical arm parks DeferredCell.
|
||||
// Must NOT throw (a park is normal, not an error). The dormant
|
||||
// stage (RuntimeSetPositionState's SubmitPreparedPlacementCore
|
||||
// deferred-commit path) already stages the body's Position/CellId
|
||||
// at the destination while it waits (StageDormantCellFrame,
|
||||
// body.InWorld=false) - the reader-visible Position moving early is
|
||||
// that mechanism, not evidence the placement committed; only
|
||||
// PlayerState/IsCollisionReady distinguish "staged" from
|
||||
// "committed" here.
|
||||
RuntimeDestinationReadiness parked = projection.PrepareDestination(
|
||||
revealGeneration: 7, destination, projectionToken);
|
||||
Assert.False(parked.IsCollisionReady);
|
||||
Assert.Equal(PlayerState.PortalSpace, controller.State);
|
||||
|
||||
// A SECOND attempt while still parked must not double-Begin -
|
||||
// Runtime's own Begin would refuse a genuine second attempt with
|
||||
// Contention, but PrepareDestination's _awaitingPortalWake polls
|
||||
// PendingCount instead of re-attempting, so this must also report
|
||||
// not-ready without throwing.
|
||||
RuntimeDestinationReadiness stillParked =
|
||||
projection.PrepareDestination(
|
||||
revealGeneration: 7, destination, projectionToken);
|
||||
Assert.False(stillParked.IsCollisionReady);
|
||||
|
||||
// Commit the destination landblock's collision generation and pump
|
||||
// the drive's wake (mirrors HeadlessSessionHost.Tick's own
|
||||
// PumpFirstEntry -> _acceptedPositionDrive.Advance() ordering).
|
||||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
||||
destinationLandblock, 1UL);
|
||||
var heights = new byte[81];
|
||||
Array.Fill(heights, (byte)50);
|
||||
var heightTable = new float[256];
|
||||
for (int index = 0; index < heightTable.Length; index++)
|
||||
heightTable[index] = index;
|
||||
runtime.EntityObjects.Physics.Engine.AddLandblock(
|
||||
destinationLandblock,
|
||||
new TerrainSurface(heights, heightTable),
|
||||
[],
|
||||
[],
|
||||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f);
|
||||
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
||||
destinationLandblock, 1UL, ready: true);
|
||||
// RuntimeAcceptedPositionDriveControllerTests.CommitLandblockCollision's
|
||||
// exact proven-working shape: the wake path's
|
||||
// resolveWorldOffsetFromRuntimeFrame requires the destination
|
||||
// landblock's world-frame offset to already be resolvable.
|
||||
runtime.EntityObjects.Physics.ObserveLocalWorldFrame(
|
||||
destinationLandblock | 0x0001u,
|
||||
teleportAdvanced: false);
|
||||
// Drain the placement projection FIFO AFTER committing (exact order
|
||||
// from RuntimeAcceptedPositionDriveControllerTests.DrainPlacementFifo's
|
||||
// call site: commit collision -> drain FIFO -> Advance) - the
|
||||
// deferred park's Withdraw notification is published as part of the
|
||||
// collision-generation commit, not before it.
|
||||
while (runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
|
||||
out RuntimePlacementProjectionSnapshot head))
|
||||
{
|
||||
if (!runtime.EntityObjects.Physics.SetPosition
|
||||
.AcknowledgeProjection(head.Token))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
acceptedPositionDrive.Advance();
|
||||
|
||||
RuntimeDestinationReadiness committed =
|
||||
projection.PrepareDestination(
|
||||
revealGeneration: 7, destination, projectionToken);
|
||||
Assert.True(committed.IsCollisionReady);
|
||||
Assert.Equal(PlayerState.InWorld, controller.State);
|
||||
Assert.Equal(10f, controller.Position.X);
|
||||
Assert.Equal(10f, controller.Position.Y);
|
||||
Assert.Equal(destinationLandblock, controller.CellId & 0xFFFF0000u);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// B1 review fix (2026-08-05): headless's required test #2 — the same
|
||||
/// unsound-commit-inference defect
|
||||
/// <see cref="HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake"/>
|
||||
/// proves the HAPPY path for, exercised on the FORGOTTEN path instead. A
|
||||
/// DeferredCell park killed by an ordinary, unrelated accepted Position
|
||||
/// merge (exactly the ACE 5-10 Hz broadcast <c>RuntimeSetPositionState</c>'s
|
||||
/// own doc names as the expected way a far-destination park resolves
|
||||
/// without committing) must leave <c>PrepareDestination</c> reporting
|
||||
/// NOT ready, <see cref="PlayerState.PortalSpace"/> unchanged, and the
|
||||
/// body never moved to the destination - before the fix,
|
||||
/// <c>PendingCount</c> hitting 0 made <c>PrepareDestination</c> infer
|
||||
/// "committed" and run the full readiness/materialize/LoginComplete
|
||||
/// sequence against an unmoved body.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void HeadlessPortalPrepareDestinationForgottenByOrdinaryMergeDoesNotLatchAsCommitted()
|
||||
{
|
||||
var operations = new FixtureSessionOperations();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
GameRuntime runtime = host.Runtime;
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
host.Start().Status);
|
||||
const uint player = 0x50000009u;
|
||||
const uint destinationLandblock = 0xAAB40000u;
|
||||
runtime.PlayerIdentity.ServerGuid = player;
|
||||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
||||
0xA9B40000u, 1UL);
|
||||
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
||||
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
||||
0xA9B40000u, 1UL, ready: true);
|
||||
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
|
||||
CreateFirstEntryDrive(runtime);
|
||||
RuntimeEntityRecord record = runtime.EntityObjects
|
||||
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
|
||||
.Canonical!;
|
||||
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||
record,
|
||||
record.CreateIntegrationVersion,
|
||||
record.Snapshot,
|
||||
replaceGeneration: false));
|
||||
var collision = new FixtureCollisionNeighborhood();
|
||||
RuntimeAcceptedPositionDriveController acceptedPositionDrive =
|
||||
CreateAcceptedPositionDrive(runtime);
|
||||
var projection = new HeadlessSessionWorldProjection(
|
||||
runtime,
|
||||
collision,
|
||||
firstEntry,
|
||||
acceptedPositionDrive);
|
||||
|
||||
projection.ProjectSpawn(record, isLocalPlayer: true);
|
||||
PlayerMovementController controller =
|
||||
Assert.IsType<PlayerMovementController>(
|
||||
runtime.MovementOwner.Controller);
|
||||
controller.SetPosition(
|
||||
new Vector3(48f, 49f, 50f),
|
||||
0xA9B40001u);
|
||||
projection.ProjectPosition(
|
||||
record,
|
||||
isLocalPlayer: true,
|
||||
PositionTimestampDisposition.Apply);
|
||||
projection.BeginTeleport();
|
||||
|
||||
var destination = new RuntimeTeleportDestination(
|
||||
player,
|
||||
InstanceSequence: 1,
|
||||
PositionSequence: 2,
|
||||
TeleportSequence: 1,
|
||||
ForcePositionSequence: 0,
|
||||
new Position(
|
||||
destinationLandblock | 0x0001u,
|
||||
new Vector3(10f, 10f, 50f),
|
||||
Quaternion.Identity));
|
||||
var projectionToken = new RuntimeWorldHostProjectionToken(
|
||||
7, destinationLandblock | 0x0001u);
|
||||
|
||||
Assert.True(runtime.EntityObjects.TryApplyPosition(
|
||||
new WorldSession.EntityPositionUpdate(
|
||||
player,
|
||||
new CreateObject.ServerPosition(
|
||||
destination.Position.ObjCellId,
|
||||
destination.Position.Frame.Origin.X,
|
||||
destination.Position.Frame.Origin.Y,
|
||||
destination.Position.Frame.Origin.Z,
|
||||
destination.Position.Frame.Orientation.W,
|
||||
destination.Position.Frame.Orientation.X,
|
||||
destination.Position.Frame.Orientation.Y,
|
||||
destination.Position.Frame.Orientation.Z),
|
||||
Velocity: null,
|
||||
PlacementId: null,
|
||||
IsGrounded: true,
|
||||
InstanceSequence: 1,
|
||||
PositionSequence: 3,
|
||||
TeleportSequence: destination.TeleportSequence,
|
||||
ForcePositionSequence: 0),
|
||||
isLocalPlayer: true,
|
||||
forcePositionRotation: Quaternion.Identity,
|
||||
currentLocalVelocity: Vector3.Zero,
|
||||
acknowledgeProjection: null,
|
||||
out _,
|
||||
out _,
|
||||
out _));
|
||||
|
||||
// First attempt: destinationLandblock's collision generation was
|
||||
// never begun/committed, so the canonical arm parks DeferredCell.
|
||||
RuntimeDestinationReadiness parked = projection.PrepareDestination(
|
||||
revealGeneration: 7, destination, projectionToken);
|
||||
Assert.False(parked.IsCollisionReady);
|
||||
Assert.Equal(PlayerState.PortalSpace, controller.State);
|
||||
Assert.Equal(1, acceptedPositionDrive.PendingCount);
|
||||
|
||||
// An ordinary, UNRELATED accepted Position for the same entity - no
|
||||
// new teleport, just a normal broadcast at the SAME already-accepted
|
||||
// teleport sequence - Forgets the parked operation the same way
|
||||
// ACE's 5-10 Hz cadence would (RuntimeSetPositionState.Forget, called
|
||||
// from TryApplyPosition for every accepted, non-Rejected Position).
|
||||
Assert.True(runtime.EntityObjects.TryApplyPosition(
|
||||
new WorldSession.EntityPositionUpdate(
|
||||
player,
|
||||
new CreateObject.ServerPosition(
|
||||
0x20210001u, 48f, 49f, 50f, 1f, 0f, 0f, 0f),
|
||||
Velocity: null,
|
||||
PlacementId: null,
|
||||
IsGrounded: true,
|
||||
InstanceSequence: 1,
|
||||
PositionSequence: 4,
|
||||
TeleportSequence: destination.TeleportSequence,
|
||||
ForcePositionSequence: 0),
|
||||
isLocalPlayer: true,
|
||||
forcePositionRotation: Quaternion.Identity,
|
||||
currentLocalVelocity: Vector3.Zero,
|
||||
acknowledgeProjection: null,
|
||||
out _,
|
||||
out _,
|
||||
out _));
|
||||
// Forget (inside TryApplyPosition) cancels the underlying
|
||||
// RuntimeSetPositionState operation immediately, but the drive's OWN
|
||||
// _pending cache only notices on its next Advance() pump - the real
|
||||
// host does this every HeadlessSessionHost.Tick via PumpFirstEntry;
|
||||
// the test drives it explicitly, same as the App-level equivalent.
|
||||
acceptedPositionDrive.Advance();
|
||||
Assert.Equal(0, acceptedPositionDrive.PendingCount);
|
||||
|
||||
// Drive well past where the pre-fix inference would have latched
|
||||
// "committed" on the very next PrepareDestination call and then
|
||||
// marched to the full readiness/materialize/LoginComplete sequence.
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
RuntimeDestinationReadiness stillNotReady =
|
||||
projection.PrepareDestination(
|
||||
revealGeneration: 7, destination, projectionToken);
|
||||
Assert.False(stillNotReady.IsCollisionReady);
|
||||
}
|
||||
|
||||
Assert.Equal(PlayerState.PortalSpace, controller.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue