acdream/tests/AcDream.App.Tests/Streaming/LocalPlayerTeleportControllerTests.cs
Erik 5ca1d47d7a feat(world): the login wormhole — every world entry runs retail's portal-space presentation with sound (TS-28 narrowed)
Retail runs the SAME TAS_TUNNEL wormhole at initial login as at an F751
teleport, with no F751 involved: SmartBox::teleport_in_progress
@0x00451C20 returns 1 the moment the login player exists with
position_update_complete == 0, gmSmartBoxUI::UseTime @0x004D6EAB
edge-detects it into BeginTeleportAnimation(TAS_TUNNEL) @0x004D6EC9
(playing Sound_UI_EnterPortal @0x004D638E), SmartBox::UseTime
@0x00455483 ends the hold once destination cells stop blocking,
Sound_UI_ExitPortal plays at the viewport swap @0x004D7405, and
LoginComplete goes out at the WorldFadeIn end @0x004D745D ->
CPlayerSystem::SendLoginCompleteNotification @0x00562E90 (ACE's own
GameActionLoginComplete comment names this contract: 'called when the
client player exits portal space. It includes initial login'). acdream
skipped all of it at login — every entry route (direct auto-select,
character-select Enter, enter-after-create) dropped onto the sky-only
'waiting for login' backdrop until the world reveal completed.

The fix engages the EXISTING F751 presentation machinery on Runtime's
login reveal — no duplicated presentation code, no timers:

- LocalPlayerTeleportController gains a login arm keyed off the
  Runtime-owned login reveal generation (RuntimeWorldTransitState
  .BeginLoginReveal, begun on the first accepted local-player position
  on every entry route). It drives the same TeleportAnimSequencer/
  PortalTunnelPresentation lifecycle and the same enter/exit cues; the
  Place edge is a no-op at login (the first-entry conductor already
  committed the canonical placement — retail's analogue only flips
  position_update_complete), and FireLoginComplete now performs
  EnterWorld + the single LoginComplete send + reveal Complete, exactly
  like the F751 pump. worldReady is latched on BOTH canonical first
  placement (OnLocalPlayerFirstEntryCompleted, the repointed
  GraphicalSessionEventRoute completion callback that used to send
  LoginComplete immediately) AND destination reveal readiness.
  ActiveDestinationCell now also reports the login destination so the
  render frame's reveal-preparation arm keeps running after portal-space
  entry flips ChaseModeEverEntered.
- PlayerModeController.TryEnterPortalSpaceForLogin performs the
  player-mode presentation attach (the same BuildControllerAndCamera the
  post-reveal auto-entry used to run) before flipping into portal space
  — at login no player-mode entry has happened yet. TryEnterPortalSpace
  itself now refuses (retryable) on a constructed-but-unpublished
  Runtime controller via the documented CanExecuteLiveMovement skip
  predicate instead of faulting — the first connected run crashed on
  exactly that pre-publication State write.
- HouseQuery stays at first-entry completion (retail: tail-called from
  CPlayerSystem::InitializePlayer @0x00563570, an object-arrival edge,
  not a tunnel edge).
- An F751 arriving mid-login-tunnel withdraws the login claim and hands
  the presentation to the portal pump, which owns the single
  LoginComplete — matching retail's one teleportInProgress flag.

TS-28 narrowed: the graphical host now runs the full login wormhole;
the residual is headless-only (no presentation; placement-edge send).

Live gates (testaccount2/+Horan vs local ACE, Release): the
character-select Enter route and the --session-config direct auto-select
route both play the wormhole with Sound_UI_EnterPortal at animation
begin, hold with retail's 'In Portal Space - Please Wait...' notice
until readiness, fade out with the view-plane warp, send LoginComplete
at the WorldFadeIn end, and materialize in Holtburg; ACE-confirmed
graceful logout. Tests: App 5493/3 skips (baseline 5490 + 3 new login
tests), Runtime 1744/0, full solution green.

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

1699 lines
70 KiB
C#

using System.Numerics;
using AcDream.App.Input;
using AcDream.App.Rendering;
using AcDream.App.Streaming;
using AcDream.App.Update;
using AcDream.App.World;
using AcDream.Content;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Physics;
using AcDream.Runtime.Session;
using AcDream.Runtime.World;
using DatReaderWriter.Enums;
namespace AcDream.App.Tests.Streaming;
public sealed class LocalPlayerTeleportControllerTests
{
[Fact]
public void DeferredNetworkOwnedBindingReleasesExactlyAndAllowsRebind()
{
var deferred = new DeferredLocalPlayerTeleportNetworkSink();
var first = new FakeNetworkSink();
var second = new FakeNetworkSink();
IDisposable firstBinding = deferred.BindOwned(first);
Assert.Throws<InvalidOperationException>(() => deferred.BindOwned(second));
firstBinding.Dispose();
using IDisposable secondBinding = deferred.BindOwned(second);
firstBinding.Dispose();
deferred.OnTeleportStarted(9);
Assert.Empty(first.Starts);
Assert.Equal([9u], second.Starts);
}
[Fact]
public void DestinationBeforeStart_IsReplayedOnceAfterPresentationActivates()
{
var harness = new Harness();
RuntimeTeleportDestination destination = Position(
cellId: 0x20210001u,
teleportSequence: 7,
x: 12f,
y: 34f,
z: 5f);
harness.OfferDestination(
destination,
teleportTimestampAdvanced: true);
Assert.Empty(harness.Streaming.Reservations);
harness.Controller.OnTeleportStarted(7);
Assert.Equal(1, harness.Input.EndCount);
Assert.Equal(1, harness.Mode.EnterPortalCount);
Assert.Equal(Matrix4x4.Identity, harness.Presentation.BeginProjection);
Assert.Equal(
(1L, 0x20210001u, harness.RevealWindow.FarRadius),
Assert.Single(harness.Streaming.Reservations));
Assert.True(harness.Reveal.Snapshot.IsActive);
Assert.Equal(RuntimePortalKind.Portal, harness.Reveal.Snapshot.Kind);
harness.OfferDestination(
destination,
teleportTimestampAdvanced: false);
Assert.Single(harness.Streaming.Reservations);
}
[Fact]
public void MissingPlayerController_KeepsStartAndDestinationPendingUntilProjectionExists()
{
var harness = new Harness();
harness.Mode.Controller = null;
harness.Controller.OnTeleportStarted(2);
harness.OfferDestination(
Position(0x20210001u, 2, 7f, 8f, 9f),
teleportTimestampAdvanced: true);
Assert.False(harness.Controller.IsActive);
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
Assert.Null(harness.Presentation.BeginProjection);
harness.Mode.Controller = new PlayerMovementController(new PhysicsEngine());
harness.Mode.Controller.SeedPlacementForTest(
Vector3.Zero,
0x20210001u,
Vector3.Zero);
harness.Controller.Tick(0.016f);
Assert.True(harness.Controller.IsActive);
Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell);
Assert.NotNull(harness.Presentation.BeginProjection);
Assert.Single(harness.Streaming.Reservations);
}
[Fact]
public void ControllerWithdrawnAfterActivation_KeepsAcceptedDestinationUntilItReturns()
{
var harness = new Harness();
harness.Controller.OnTeleportStarted(14);
Assert.True(harness.Controller.IsActive);
harness.Mode.Controller = null;
harness.OfferDestination(
Position(0x20210001u, 14, 7f, 8f, 9f),
teleportTimestampAdvanced: true);
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
Assert.Empty(harness.Streaming.Reservations);
harness.Mode.Controller = new PlayerMovementController(new PhysicsEngine());
harness.Mode.Controller.SeedPlacementForTest(Vector3.Zero, 0x20210001u, Vector3.Zero);
harness.Controller.Tick(0.016f);
Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell);
Assert.Single(harness.Streaming.Reservations);
}
// C4 route 3 root-caused (closed): the earlier "bare-fixture InvalidData"
// diagnosis was WRONG - these never reached
// RuntimeSetPositionMoverPreparer.TryBuild at all. The real defect was in
// LocalPlayerTeleportController.TryExecuteCanonicalPortalPlacement: it
// re-read _transit.TryGetAcceptedTeleportDestination at Place time, but
// RuntimeWorldTransitState.TryBeginPortalReveal (driven by AimDestination
// via WorldRevealCoordinator.TryBeginPortal) atomically CONSUMES that one
// accepted-destination slot the instant Aim claims the reveal generation
// - so the Place-time re-read always found it empty and refused with
// "cause=host-token-unavailable" before ever building a mover. Fixed by
// caching the destination at Aim time (_pendingDestination), mirroring
// the existing _pendingCell/_pendingRotation/_pendingRevealGeneration
// pattern in the same method. Production fix, not a fixture workaround.
[Fact]
public void ControllerWithdrawnAfterAim_HoldsPresentationUntilModeRebuildsIt()
{
var harness = new Harness();
harness.Controller.OnTeleportStarted(15);
harness.OfferDestination(
Position(0x20210001u, 15, 7f, 8f, 9f),
teleportTimestampAdvanced: true);
harness.Mode.Controller = null;
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
harness.Controller.Tick(0.016f);
Assert.False(harness.Placement.Called);
Assert.Empty(harness.Presentation.WorldReadyValues);
harness.Mode.RebuildOnEnter = () =>
{
var controller = new PlayerMovementController(new PhysicsEngine());
controller.SeedPlacementForTest(Vector3.Zero, 0x20210001u, Vector3.Zero);
return controller;
};
harness.Controller.Tick(0.016f);
Assert.NotNull(harness.Mode.Controller);
Assert.True(harness.Placement.Called);
// A6 review fix: assert the REAL canonical body resolved the
// offered destination (7,8,9), not just that the acknowledge-only
// suffix ran.
Assert.Equal(
new Vector3(7f, 8f, 9f),
harness.Movement.Controller!.Position);
Assert.Equal(0x20210001u, harness.Movement.Controller.CellId);
}
[Fact]
public void SameLandblockDestination_DoesNotRecenterAndKeepsTranslatedPosition()
{
var harness = new Harness(centerX: 0x20, centerY: 0x21);
// 0x20210123 is an INDOOR cell (low word 0x0123 >= 0x0100) within the
// same landblock the constructor already committed. It still needs
// its own synthetic CellStruct - PhysicsEngine.IsSpawnCellReady
// requires DataCache.GetCellStruct to resolve for indoor cells, and
// a bare AddLandblock never populates one.
harness.AddSyntheticIndoorCell(0x20210123u);
harness.Controller.OnTeleportStarted(3);
harness.OfferDestination(
Position(0x20210123u, 3, 20f, 30f, 4f),
teleportTimestampAdvanced: true);
harness.Presentation.EmitPlaceWhenReady = true;
harness.Controller.Tick(0.016f);
Assert.Empty(harness.Streaming.Recenters);
// C4 route 3: the canonical Runtime arm - not this fake - resolves
// and commits the destination; the acknowledge-only suffix carries
// no position/cell to capture (D-T4). The observable fact left at
// this layer is that the committed receipt reached the suffix.
Assert.True(harness.Placement.Called);
// A6/R4 review fix: assert the REAL canonical body resolved the
// offered destination, not just that the suffix ran.
Assert.Equal(
new Vector3(20f, 30f, 4f),
harness.Movement.Controller!.Position);
Assert.Equal(0x20210123u, harness.Movement.Controller.CellId);
}
/// <summary>
/// A1/R1 review fix (2026-08-05): the retail/architecture review's own
/// required test — "refused arm -&gt; the anim stream does not reach
/// RevealWorldViewport/FireLoginComplete with an unplaced body." Forces
/// a genuine Runtime-level Contention refusal (a competing operation
/// already owns the entity's placement token, same shape as the Runtime
/// layer's <c>PortalContention_WhenTheEntityAlreadyOwnsAnActiveOperation</c>)
/// and drives several ticks past where the OLD unconditional-march code
/// would have already fired LoginComplete. The player must still be
/// standing at the origin, in portal space, with the transit still
/// active — never released into the world unplaced.
/// </summary>
[Fact]
public void RefusedPlace_HoldsTheStreamAndConvergesOnlyAfterContentionClears()
{
var harness = new Harness();
harness.Controller.OnTeleportStarted(50);
harness.OfferDestination(
Position(0x20210001u, 50, 11f, 12f, 13f),
teleportTimestampAdvanced: true);
harness.Presentation.EmitPlaceWhenReady = true;
Vector3 positionBefore = harness.Movement.Controller!.Position;
// A competing operation already owns the entity's placement token -
// the canonical Runtime Begin call inside TryAdvancePortalCommit
// refuses with Contention, exactly like the Runtime-layer
// PortalContention_WhenTheEntityAlreadyOwnsAnActiveOperation test.
RuntimeEntityRecord record = harness.Lifetime.Entities
.TryGetActive(0x50000001u, out RuntimeEntityRecord active)
? active
: throw new InvalidOperationException("fixture entity missing");
RuntimeEntityPlacementToken displaced = harness.Lifetime.Physics
.SetPosition.TryBeginExclusiveAuthoredPlacement(
record,
record.PositionAuthorityVersion,
RuntimeSetPositionOperationKind.LocalAuthoritative);
Assert.True(displaced.IsValid);
// Several ticks, well past where the pre-fix code would already
// have reached PlayExitSound -> WorldFadeIn -> FireLoginComplete
// and reset the transit (the tunnel's own retail timing is 2-5s;
// this drives 3x that in 0.1s steps).
for (int i = 0; i < 100; i++)
harness.Controller.Tick(0.1f);
Assert.False(harness.Placement.Called);
Assert.Equal(positionBefore, harness.Movement.Controller.Position);
Assert.True(harness.Controller.IsActive);
Assert.False(harness.Reveal.Snapshot.Completed);
Assert.Equal(0, harness.Session.LoginCompleteCount);
// Release the competing operation - the SAME `dataReady` predicate
// that already held the stream in Tunnel now lets the commit
// succeed on the very next tick (D-T5 row 2's "next Tick
// re-attempts the Place edge", made real without ever touching
// TeleportAnimSequencer).
RuntimePlacementCancellationReceipt cancellation = harness.Lifetime
.Physics.SetPosition.ForgetExactPlacement(displaced);
if (cancellation.IsValid)
harness.Lifetime.Physics.SetPosition.PublishCancellation(cancellation);
harness.Controller.Tick(0.1f);
Assert.True(harness.Placement.Called);
Assert.Equal(
new Vector3(11f, 12f, 13f),
harness.Movement.Controller.Position);
}
/// <summary>
/// B1 review fix (2026-08-05): the required test #1 — a DeferredCell
/// park killed by an ORDINARY merge-time <c>Forget</c> (not a
/// collision-generation commit) must NOT be latched as "committed".
/// The destination landblock is never committed
/// (<c>CommitLandblockCollision</c> is deliberately not called), so the
/// portal parks; an unrelated ordinary accepted Position for the same
/// entity then Forgets the outstanding operation exactly like a normal
/// 5-10 Hz ACE broadcast would — <c>RuntimeSetPositionState</c>'s own
/// doc names this the EXPECTED way a far-destination park resolves
/// without committing. Before the fix, <c>PendingCount</c> hitting 0
/// made the host gate latch <c>_placementCommitted = true</c>, release
/// the sequencer, and run the ENTIRE completion sequence
/// (materialize/reveal/LoginComplete) against the unmoved body.
/// </summary>
[Fact]
public void ParkedPlace_ForgottenByOrdinaryMergeDoesNotLatchAsCommitted()
{
var harness = new Harness();
const uint destinationLandblock = 0x40410000u;
harness.Controller.OnTeleportStarted(60);
harness.OfferDestination(
Position(destinationLandblock | 0x0001u, 60, 21f, 22f, 23f),
teleportTimestampAdvanced: true);
harness.Presentation.EmitPlaceWhenReady = true;
// First tick: destinationLandblock's collision generation was never
// committed, so the canonical arm parks DeferredCell.
harness.Controller.Tick(0.016f);
Assert.False(harness.Placement.Called);
Assert.Equal(1, harness.AcceptedPositionDrive.PendingCount);
// An ordinary, UNRELATED accepted Position for the same entity -
// no new teleport, just a normal broadcast - Forgets the parked
// operation the same way ACE's 5-10 Hz cadence would.
harness.MergeOrdinaryPosition(
new Vector3(48f, 49f, 50f), 0x20210001u, teleportSequence: 60);
// Forget (inside TryApplyPosition) cancels the underlying
// RuntimeSetPositionState operation immediately, but the drive's
// OWN _pending cache only notices on its next Advance() pump - the
// App host's real per-frame retry lease does this; the test drives
// it explicitly.
harness.AcceptedPositionDrive.Advance();
Assert.Equal(0, harness.AcceptedPositionDrive.PendingCount);
// Drive well past where the pre-fix inference would have latched
// "committed" on the very next tick and then marched to
// LoginComplete.
for (int i = 0; i < 100; i++)
harness.Controller.Tick(0.1f);
// NOTE: the body's Position/CellId are NOT asserted unchanged here.
// The DeferredCell park's dormant stage (RuntimeSetPositionState's
// SubmitPreparedPlacementCore deferred-commit path,
// StageDormantCellFrame) already writes the staged destination onto
// the body's controller while body.InWorld stays false and the
// canonical placement has not committed - the reader-visible
// Position/CellId moving early is that benign staging mechanism, not
// evidence the placement committed (see the analogous headless test
// HeadlessPortalPrepareDestinationParksThenCommitsOnCollisionGenerationWake
// for the same finding). The discriminators below - the presentation
// suffix never running, no materialize/reveal/LoginComplete, and the
// controller staying active in an incomplete transit - are what
// actually distinguish "committed" from "parked-then-forgotten".
Assert.False(harness.Placement.Called);
Assert.Equal(0, harness.Reveal.PortalMaterializationCount);
Assert.Equal(0, harness.Session.LoginCompleteCount);
Assert.True(harness.Controller.IsActive);
Assert.False(harness.Reveal.Snapshot.Completed);
}
[Fact]
public void CrossLandblockDestination_RecentersBeforeItCanBecomeReady()
{
var harness = new Harness(centerX: 0x20, centerY: 0x21);
harness.CommitLandblockCollision(0x30310000u);
// 0x30310100 is an INDOOR cell (low word 0x0100 >= 0x0100); see the
// AddSyntheticIndoorCell doc comment for why CommitLandblockCollision
// alone (terrain-only) cannot make it spawn-ready.
harness.AddSyntheticIndoorCell(0x30310100u);
harness.Mode.Controller!.SeedPlacementForTest(
Vector3.Zero,
0x20210001u,
Vector3.Zero);
harness.Controller.OnTeleportStarted(4);
harness.Streaming.RecenterPending = true;
harness.OfferDestination(
Position(0x30310100u, 4, -2f, 8f, 9f),
teleportTimestampAdvanced: true);
harness.Presentation.EmitPlaceWhenReady = true;
harness.Controller.Tick(1f);
Assert.Equal((0x30, 0x31, true), Assert.Single(harness.Streaming.Recenters));
Assert.False(Assert.Single(harness.Presentation.WorldReadyValues));
Assert.False(harness.Placement.Called);
harness.Streaming.RecenterPending = false;
harness.Controller.Tick(0.016f);
Assert.True(harness.Placement.Called);
// A6/R4 review fix: assert the REAL canonical body resolved the
// cross-landblock destination, not just that the suffix ran.
Assert.Equal(
new Vector3(-2f, 8f, 9f),
harness.Movement.Controller!.Position);
Assert.Equal(0x30310100u, harness.Movement.Controller.CellId);
}
[Fact]
public void ReadinessHold_NeverRevealsIncompleteWorldAndShowsWaitCueAfterFiveSeconds()
{
var harness = new Harness(worldReady: false);
harness.Controller.OnTeleportStarted(5);
harness.OfferDestination(
Position(0x20210001u, 5, 1f, 2f, 3f),
teleportTimestampAdvanced: true);
harness.Presentation.EmitPlaceWhenReady = true;
harness.Controller.Tick(4.9f);
Assert.False(Assert.Single(harness.Presentation.WorldReadyValues));
Assert.False(harness.Presentation.WaitCueValues[^1]);
Assert.False(harness.Placement.Called);
harness.Controller.Tick(0.1f);
harness.Controller.Tick(30f);
Assert.All(harness.Presentation.WorldReadyValues, Assert.False);
Assert.True(harness.Presentation.WaitCueValues[^1]);
Assert.False(harness.Placement.Called);
Assert.True(harness.Reveal.WaitCueShown);
}
[Fact]
public void Place_ReconcilesInsidePlacementBeforeRevealMaterialized()
{
var order = new List<string>();
var harness = new Harness(order: order);
harness.Controller.OnTeleportStarted(8);
harness.OfferDestination(
Position(0x20210001u, 8, 4f, 5f, 6f),
teleportTimestampAdvanced: true);
order.Clear();
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
harness.Controller.Tick(0.016f);
Assert.True(Index(order, "placement") < Index(order, "[world-reveal] event=materialized"));
Assert.Equal(1, harness.Reveal.PortalMaterializationCount);
}
[Fact]
public void Place_RechecksExactRuntimeLifetimeBeforeMutatingPresentation()
{
var harness = new Harness();
harness.Controller.OnTeleportStarted(81);
harness.OfferDestination(
Position(0x20210001u, 81, 4f, 5f, 6f),
teleportTimestampAdvanced: true);
long generation = harness.Reveal.Snapshot.Generation;
Assert.True(harness.Transit.Cancel(generation));
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
harness.Controller.Tick(0.016f);
Assert.False(harness.Placement.Called);
Assert.Equal(0, harness.Reveal.PortalMaterializationCount);
}
[Fact]
public void LoginComplete_EntersWorldThenSendsThenCompletesAndResets()
{
var order = new List<string>();
var harness = new Harness(order: order);
harness.Controller.OnTeleportStarted(9);
harness.OfferDestination(
Position(0x20210001u, 9, 4f, 5f, 6f),
teleportTimestampAdvanced: true);
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
harness.Controller.Tick(0.016f);
order.Clear();
harness.Presentation.Enqueue(TeleportAnimEvent.FireLoginComplete);
harness.Controller.Tick(0.016f);
Assert.True(Index(order, "enter-world") < Index(order, "login-complete"));
Assert.True(Index(order, "login-complete") < Index(order, "[world-reveal] event=complete"));
Assert.True(Index(order, "[world-reveal] event=complete") < Index(order, "presentation-reset"));
Assert.False(harness.Controller.IsActive);
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
Assert.Equal(1, harness.Session.LoginCompleteCount);
Assert.True(harness.Reveal.Snapshot.Completed);
Assert.False(harness.Reveal.Snapshot.Cancelled);
}
[Fact]
public void ExitSound_ReleasesDestinationBeforeHidingTunnelButKeepsProtocolActive()
{
var order = new List<string>();
var harness = new Harness(order: order);
harness.Controller.OnTeleportStarted(91);
harness.OfferDestination(
Position(0x20210001u, 91, 4f, 5f, 6f),
teleportTimestampAdvanced: true);
harness.Streaming.ReservationEnds.Clear();
harness.Presentation.Enqueue(TeleportAnimEvent.PlayExitSound);
harness.Controller.Tick(0.016f);
Assert.False(harness.Presentation.IsPortalViewportVisible);
Assert.True(harness.Controller.IsActive);
Assert.False(harness.Reveal.Snapshot.Completed);
Assert.Single(harness.Streaming.ReservationEnds);
}
[Fact]
public void NewerStart_ReplacesOldDestinationWithoutReusingIt()
{
var harness = new Harness();
harness.Controller.OnTeleportStarted(10);
harness.OfferDestination(
Position(0x20210001u, 10, 1f, 1f, 1f),
teleportTimestampAdvanced: true);
harness.Controller.OnTeleportStarted(11);
harness.Controller.Tick(30f);
Assert.False(harness.Placement.Called);
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
harness.OfferDestination(
Position(0x20210001u, 11, 2f, 2f, 2f),
teleportTimestampAdvanced: true);
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
harness.Controller.Tick(0.016f);
Assert.True(harness.Placement.Called);
// A6 review fix: this is the specific test A6 named — the removed
// assertion was the ONLY thing proving the SECOND destination
// (2,2,2) placed and not the superseded first one (1,1,1).
// Assert.True(Placement.Called) alone cannot distinguish them. X/Y
// are exact; Z settles to the Harness's uniform terrain height (5)
// because both candidate wire Z values (1 and 2) sit BELOW the
// floor and the placement resolve's anti-penetration push-out is
// instant (unlike gravity settling from ABOVE the floor, which
// takes further physics ticks this test never runs) - X/Y alone
// already discriminate (1,1) from (2,2) unambiguously.
Assert.Equal(2f, harness.Movement.Controller!.Position.X);
Assert.Equal(2f, harness.Movement.Controller.Position.Y);
Assert.Equal(0x20210001u, harness.Movement.Controller.CellId);
}
[Fact]
public void SessionReset_ClearsTransitAndLetsRecenterConvergeAcrossLaterFrames()
{
var harness = new Harness();
harness.Controller.OnTeleportStarted(12);
harness.OfferDestination(
Position(0x20210001u, 12, 1f, 2f, 3f),
teleportTimestampAdvanced: true);
harness.Controller.ResetSession();
Assert.False(harness.Controller.IsActive);
Assert.Equal(RuntimePortalSnapshot.Idle, harness.Reveal.Snapshot);
Assert.True(harness.Streaming.LastResetWasSessionEnding);
var failed = new Harness();
failed.Streaming.ResetResult = false;
failed.Controller.ResetSession();
Assert.False(failed.Controller.IsActive);
Assert.True(failed.Streaming.LastResetWasSessionEnding);
}
[Fact]
public void StaleStart_DoesNotMutateInputModeOrPresentation()
{
var harness = new Harness();
harness.Authority.IsFresh = false;
harness.Controller.OnTeleportStarted(13);
Assert.Equal(0, harness.Input.EndCount);
Assert.Equal(0, harness.Mode.EnterPortalCount);
Assert.Null(harness.Presentation.BeginProjection);
Assert.False(harness.Controller.IsActive);
}
[Fact]
public void ReentrantNewStartDuringPlacement_CannotBeCompletedByOldTail()
{
var harness = new Harness();
harness.Controller.OnTeleportStarted(20);
harness.OfferDestination(
Position(0x20210001u, 20, 1f, 2f, 3f),
teleportTimestampAdvanced: true);
harness.Streaming.ReservationEnds.Clear();
harness.Placement.OnPlace = () => harness.Controller.OnTeleportStarted(21);
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
harness.Controller.Tick(0.016f);
Assert.True(harness.Controller.IsActive);
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
Assert.Equal(0, harness.Reveal.PortalMaterializationCount);
Assert.Single(harness.Streaming.ReservationEnds);
}
[Fact]
public void ReentrantNewStartDuringLoginComplete_PreservesReplacementLifetime()
{
var harness = new Harness();
harness.Controller.OnTeleportStarted(30);
harness.OfferDestination(
Position(0x20210001u, 30, 1f, 2f, 3f),
teleportTimestampAdvanced: true);
harness.Session.OnSend = () => harness.Controller.OnTeleportStarted(31);
harness.Presentation.Enqueue(TeleportAnimEvent.FireLoginComplete);
harness.Controller.Tick(0.016f);
Assert.True(harness.Controller.IsActive);
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
Assert.False(harness.Reveal.Snapshot.Completed);
Assert.Equal(1, harness.Session.LoginCompleteCount);
}
[Fact]
public void DisposeFailure_CanBeRetriedWithoutDoubleDisposingAfterSuccess()
{
var harness = new Harness();
harness.Presentation.DisposeFailuresRemaining = 1;
Assert.Throws<InvalidOperationException>(() => harness.Controller.Dispose());
harness.Controller.Dispose();
harness.Controller.Dispose();
Assert.Equal(2, harness.Presentation.DisposeCount);
}
[Fact]
public void ConcretePlacement_CommitsEntityControllerAndOneSpatialReconcile()
{
const uint guid = 0x50000001u;
const uint cell = 0x20210001u;
var world = new GpuWorldState();
world.AddLandblock(new LoadedLandblock(
0x2021FFFFu,
new DatReaderWriter.DBObjs.LandBlock(),
Array.Empty<WorldEntity>()));
var runtime = LiveEntityRuntimeFixture.Create(world, new NullResources());
runtime.RegisterLiveEntity(Spawn(guid, cell));
WorldEntity entity = runtime.MaterializeLiveEntity(
guid,
cell,
id => new WorldEntity
{
Id = id,
ServerGuid = guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
var identity = new LocalPlayerIdentityState { ServerGuid = guid };
var controllerSlot = new RuntimeLocalPlayerMovementState
{
Controller = new PlayerMovementController(new PhysicsEngine()),
};
var position = new Vector3(12f, 24f, 6f);
Quaternion rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.5f);
// C4 route 3: the canonical Runtime commit (RuntimeSetPositionState
// .CommitCanonical, driven by RuntimeAcceptedPositionDriveController
// .ReconcileAndAcknowledgePortal -> PlayerMovementController
// .CommitCanonicalTeleportFrame in production) already resolves the
// body/cell/orientation BEFORE this suffix runs. Reproduce that
// pre-state directly rather than re-resolving it here - Place() no
// longer resolves anything (D-T4).
controllerSlot.Controller.SeedPlacementForTest(position, cell, position);
controllerSlot.Controller.SetBodyOrientation(rotation);
var cameras = new ChaseCameraInputState
{
Legacy = new ChaseCamera(),
Retail = new RetailChaseCamera(),
};
var spatial = new FakeSpatialReconcile(() => new PlacementSnapshot(
entity.Position,
entity.ParentCellId ?? 0u,
entity.Rotation,
controllerSlot.Controller.Position,
controllerSlot.Controller.CellId,
controllerSlot.Controller.BodyOrientation));
var placement = new LocalPlayerTeleportPlacement(
runtime,
identity,
controllerSlot,
new LocalPlayerPhysicsHostSlot(),
cameras,
spatial);
placement.Place(rotation);
Assert.Equal(entity.Position, controllerSlot.Controller.Position);
Assert.Equal(entity.ParentCellId, controllerSlot.Controller.CellId);
Assert.Equal(cell & 0xFFFF0000u, entity.ParentCellId & 0xFFFF0000u);
Assert.Equal(rotation, entity.Rotation);
Assert.Equal(rotation, controllerSlot.Controller.BodyOrientation);
Assert.Equal(1, spatial.Count);
Assert.Equal(entity.Position, spatial.Snapshot.EntityPosition);
Assert.Equal(entity.ParentCellId, spatial.Snapshot.EntityCell);
Assert.Equal(entity.Rotation, spatial.Snapshot.EntityRotation);
Assert.Equal(
controllerSlot.Controller.Position,
spatial.Snapshot.ControllerPosition);
Assert.Equal(
controllerSlot.Controller.CellId,
spatial.Snapshot.ControllerCell);
Assert.Equal(
controllerSlot.Controller.BodyOrientation,
spatial.Snapshot.ControllerRotation);
}
[Fact]
public void ConcretePlacement_CommitsDestinationSpatialBucketBeforeReconcile()
{
const uint guid = 0x50000001u;
const uint sourceCell = 0x20210001u;
const uint destinationCell = 0x30310001u;
var world = new GpuWorldState();
world.AddLandblock(new LoadedLandblock(
sourceCell & 0xFFFF0000u | 0xFFFFu,
new DatReaderWriter.DBObjs.LandBlock(),
Array.Empty<WorldEntity>()));
world.AddLandblock(new LoadedLandblock(
destinationCell & 0xFFFF0000u | 0xFFFFu,
new DatReaderWriter.DBObjs.LandBlock(),
Array.Empty<WorldEntity>()));
var runtime = LiveEntityRuntimeFixture.Create(world, new NullResources());
runtime.RegisterLiveEntity(Spawn(guid, sourceCell));
WorldEntity entity = runtime.MaterializeLiveEntity(
guid,
sourceCell,
id => new WorldEntity
{
Id = id,
ServerGuid = guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
})!;
var identity = new LocalPlayerIdentityState { ServerGuid = guid };
var controllerSlot = new RuntimeLocalPlayerMovementState
{
Controller = new PlayerMovementController(new PhysicsEngine()),
};
controllerSlot.Controller.SeedPlacementForTest(
Vector3.Zero,
sourceCell,
Vector3.Zero);
// C4 route 3: reproduce the canonical Runtime commit's pre-state
// (body/cell already resolved) - Place() no longer resolves it.
var destinationPosition = new Vector3(12f, 24f, 6f);
controllerSlot.Controller.SeedPlacementForTest(
destinationPosition,
destinationCell,
destinationPosition);
var cameras = new ChaseCameraInputState
{
Legacy = new ChaseCamera(),
Retail = new RetailChaseCamera(),
};
LiveEntityRecord? recordAtReconcile = null;
var spatial = new FakeSpatialReconcile(() =>
{
Assert.True(runtime.TryGetRecord(guid, out recordAtReconcile));
return new PlacementSnapshot(
entity.Position,
entity.ParentCellId ?? 0u,
entity.Rotation,
controllerSlot.Controller.Position,
controllerSlot.Controller.CellId,
controllerSlot.Controller.BodyOrientation);
});
var placement = new LocalPlayerTeleportPlacement(
runtime,
identity,
controllerSlot,
new LocalPlayerPhysicsHostSlot(),
cameras,
spatial);
placement.Place(Quaternion.Identity);
Assert.NotNull(recordAtReconcile);
uint resolvedDestinationCell = controllerSlot.Controller.CellId;
Assert.Equal(
destinationCell & 0xFFFF0000u,
resolvedDestinationCell & 0xFFFF0000u);
Assert.Equal(resolvedDestinationCell, recordAtReconcile.FullCellId);
Assert.Equal(
destinationCell & 0xFFFF0000u | 0xFFFFu,
recordAtReconcile.CanonicalLandblockId);
Assert.True(recordAtReconcile.IsSpatiallyProjected);
Assert.True(recordAtReconcile.IsSpatiallyVisible);
Assert.Contains(entity, world.Entities);
Assert.Equal(resolvedDestinationCell, entity.ParentCellId);
Assert.Equal(1, spatial.Count);
}
private static int Index(IReadOnlyList<string> events, string prefix)
{
for (int i = 0; i < events.Count; i++)
{
if (events[i].StartsWith(prefix, StringComparison.Ordinal))
return i;
}
return int.MaxValue;
}
private static RuntimeTeleportDestination Position(
uint cellId,
ushort teleportSequence,
float x,
float y,
float z) => new(
EntityGuid: 0x50000001u,
InstanceSequence: 1,
PositionSequence: 1,
TeleportSequence: teleportSequence,
ForcePositionSequence: 1,
Position: new Position(
cellId,
new Vector3(x, y, z),
Quaternion.Identity));
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell) => new WorldSession.EntitySpawn(
Guid: guid,
Position: new CreateObject.ServerPosition(
cell,
0f,
0f,
0f,
1f,
0f,
0f,
0f),
SetupTableId: 0x02000001u,
AnimPartChanges: [],
TextureChanges: [],
SubPalettes: [],
BasePaletteId: null,
ObjScale: null,
Name: "player",
ItemType: null,
MotionState: null,
MotionTableId: null).WithConsistentPhysics();
private sealed class NullResources : ILiveEntityResourceLifecycle
{
public void Register(WorldEntity entity) { }
public void Unregister(WorldEntity entity) { }
}
private sealed class FakeSpatialReconcile : ILiveSpatialReconcilePhase
{
private readonly Func<PlacementSnapshot> _capture;
public FakeSpatialReconcile(Func<PlacementSnapshot> capture) =>
_capture = capture;
public int Count;
public PlacementSnapshot Snapshot;
public void Reconcile()
{
Snapshot = _capture();
Count++;
}
}
private readonly record struct PlacementSnapshot(
Vector3 EntityPosition,
uint EntityCell,
Quaternion EntityRotation,
Vector3 ControllerPosition,
uint ControllerCell,
Quaternion ControllerRotation);
/// <summary>
/// C4 route 3: the Place edge now drives a REAL
/// <see cref="RuntimeAcceptedPositionDriveController"/> portal arm
/// against a REAL (bare, session-less) local-player canonical record -
/// the class this file's <c>LocalPlayerTeleportController</c> fake
/// wiring cannot substitute (it is a sealed Runtime type, not an
/// interface). <see cref="OfferDestination"/> also performs the SAME
/// upstream merge production runs before ever offering a destination
/// (<c>RuntimeEntityObjectLifetime.TryApplyPosition</c>, called by
/// <c>LiveEntityNetworkUpdateController.OnPosition</c> before
/// <c>OfferDestination</c>) so <c>BeginAcceptedPlacementCore</c>'s
/// portal-vs-latest-cell gate sees the SAME destination the offer
/// carries - exactly the production ordering, not a shortcut.
/// </summary>
private sealed class Harness
{
private const uint PlayerGuid = 0x50000001u;
private const uint HomeCell = 0x20210001u;
private ushort _mergePositionSequence = 1;
public readonly FakeAuthority Authority = new();
public readonly FakeInput Input = new();
public readonly FakeMode Mode;
public readonly FakeStreaming Streaming;
public readonly FakePlacement Placement;
public readonly FakeSession Session;
public readonly FakePresentation Presentation;
public readonly RuntimeWorldTransitState Transit;
public readonly WorldRevealCoordinator Reveal;
/// <summary>
/// The live streaming window this harness's reveal barrier derives its
/// outdoor radius from (#280). Assertions read this rather than a
/// literal so the test cannot silently re-encode the value under test.
/// </summary>
public StreamingRevealWindow RevealWindow { get; set; } =
new(NearRadius: 1, FarRadius: 1);
public readonly LocalPlayerTeleportController Controller;
public readonly RuntimeEntityObjectLifetime Lifetime;
public readonly RuntimeAcceptedPositionDriveController AcceptedPositionDrive;
/// <summary>
/// A6/R4/A5 review fix (2026-08-05): exposed so tests can assert the
/// REAL canonical body's resolved position/cell after a committed
/// portal placement, not just <c>Placement.Called</c> — the property
/// that let A6's superseded-teleport test lose its destination
/// discriminator (which of two destinations actually placed).
/// </summary>
public readonly RuntimeLocalPlayerMovementState Movement;
public IPreparedCollisionSource DiagnosticCollisionSource => new UnusedCollisionSource();
/// <summary>
/// Enter-world round (2026-08-17): mutable so a login-arm test can
/// flip destination readiness mid-flight (the login tunnel's hold is
/// exactly "readiness not yet true"). Initialized from the ctor's
/// existing <c>worldReady</c> parameter, so every prior test reads
/// identically.
/// </summary>
public bool WorldReady;
public Harness(
int centerX = 0x20,
int centerY = 0x21,
bool worldReady = true,
List<string>? order = null)
{
WorldReady = worldReady;
order ??= new List<string>();
Mode = new FakeMode(order);
Streaming = new FakeStreaming(centerX, centerY, order);
Placement = new FakePlacement(order);
Session = new FakeSession(order);
Presentation = new FakePresentation(order);
Transit = new RuntimeWorldTransitState(order.Add);
Reveal = new WorldRevealCoordinator(
Transit,
revealWindow: () => RevealWindow,
isRenderNeighborhoodReady: (_, _, _) => WorldReady,
isSpawnCellReady: _ => WorldReady,
isTerrainNeighborhoodReady: (_, _) => WorldReady,
areCompositeTexturesReady: () => WorldReady,
prepareCompositeTextures: (_, _) => { },
invalidateCompositeTextures: () => { },
isSpawnClaimUnhydratable: _ => false,
streaming: Streaming);
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
var heights = new byte[81];
Array.Fill(heights, (byte)5f);
var heightTable = new float[256];
for (int i = 0; i < heightTable.Length; i++)
heightTable[i] = i;
engine.AddLandblock(
HomeCell & 0xFFFF0000u,
new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
Lifetime = new RuntimeEntityObjectLifetime(engine);
// The bare AddLandblock above only seeds the ENGINE's terrain -
// TryPrepareAndSubmitAuthoredPlacement's DeferredCell park keys
// off the SEPARATE collision-GENERATION ledger
// (RuntimeSetPositionState.BeginCollisionGeneration/
// CommitCollisionGeneration), which a bare AddLandblock never
// touches. Commit it too, or every placement parks forever.
Lifetime.Physics.SetPosition.BeginCollisionGeneration(
HomeCell & 0xFFFF0000u, 1UL);
Lifetime.Physics.SetPosition.CommitCollisionGeneration(
HomeCell & 0xFFFF0000u, 1UL, ready: true);
Lifetime.Physics.ObserveLocalWorldFrame(HomeCell, teleportAdvanced: false);
Lifetime.BindEventContext(
static () => new RuntimeGenerationToken(1UL),
static () => 1UL);
Movement = new RuntimeLocalPlayerMovementState();
RuntimeLocalPlayerMovementState movement = Movement;
var identity = new RuntimeLocalPlayerIdentityState
{
ServerGuid = PlayerGuid,
};
movement.AttachPhysicsPublication(new RuntimeLocalPlayerPhysicsPublicationState(
Lifetime.Entities, Lifetime.Physics, movement, identity));
Lifetime.LocalPlayerFirstEntry.BindPublication(movement.PhysicsPublication);
Lifetime.BindLiveInputs(() => false, () => movement.Controller?.Position);
var clock = new GameRuntimeClock();
// C4 route 3 fixture note: RuntimeFirstEntryDriveController's
// ctor subscribes to RuntimeEntityObjectLifetime
// .BindInitialResidenceBeginNotification - it must exist BEFORE
// RegisterEntityWithInitialResidence runs, or it misses the
// notification and never learns the residence exists at all
// (production constructs it once per host session, ahead of
// every Create).
var firstEntry = new RuntimeFirstEntryDriveController(
Lifetime,
clock,
new UnusedCollisionSource(),
() => PlayerMovementConstructionOptions.Fallback,
static _ => new RuntimeLocalPlayerPhysicsActivationPreparation(
0.48f, 1.835f, RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
// Production's OnSpawned shape: RegisterEntityWithInitialResidence
// then ApplyAcceptedSpawn (RuntimeLiveEntitySessionController.cs).
RuntimeEntityRegistrationResult registration =
Lifetime.RegisterEntityWithInitialResidence(
Spawn(PlayerGuid, HomeCell), isLocalPlayer: true);
RuntimeEntityRecord canonical = registration.Canonical
?? throw new InvalidOperationException(
"fixture failed to register the local player");
Lifetime.ApplyAcceptedSpawn(
canonical,
canonical.CreateIntegrationVersion,
canonical.Snapshot,
replaceGeneration: registration.Inbound.Disposition
is CreateObjectTimestampDisposition.NewGeneration);
for (int attempt = 0; attempt < 8 && firstEntry.PendingCount != 0; attempt++)
{
firstEntry.DriveAll();
DrainPlacementFifo();
}
Assert.Equal(0, firstEntry.PendingCount);
Assert.True(
Lifetime.Entities.TryGetActive(PlayerGuid, out RuntimeEntityRecord seeded),
"fixture failed to register the local player");
Assert.False(
Lifetime.TryGetInitialCreateResidence(seeded, out _),
"fixture left the local player's initial-create residence open");
AcceptedPositionDrive = new RuntimeAcceptedPositionDriveController(
Lifetime,
clock,
new UnusedCollisionSource(),
new LocalPlayerOutboundController((_, _, _, _, _, _) => { }),
() => new RuntimeGenerationToken(1UL),
() => PlayerGuid,
() => movement.Controller,
() => false,
() => null,
() => movement);
Controller = new LocalPlayerTeleportController(
Authority,
Input,
Mode,
Streaming,
Transit,
Reveal,
Placement,
Session,
Presentation,
AcceptedPositionDrive);
}
/// <summary>
/// Merges the destination into the SAME canonical record the portal
/// arm reads from, exactly like production's upstream merge, then
/// offers it to the controller.
/// </summary>
public void OfferDestination(
RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced)
{
_mergePositionSequence++;
var update = new WorldSession.EntityPositionUpdate(
destination.EntityGuid,
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: _mergePositionSequence,
TeleportSequence: destination.TeleportSequence,
ForcePositionSequence: 0);
Lifetime.TryApplyPosition(
update,
isLocalPlayer: true,
forcePositionRotation: Quaternion.Identity,
currentLocalVelocity: Vector3.Zero,
acknowledgeProjection: null,
out _,
out _,
out _);
Controller.OfferDestination(destination, teleportTimestampAdvanced);
}
/// <summary>
/// B1 review fix test support (2026-08-05): merges an ORDINARY
/// accepted Position for the local player — no portal offer, no
/// transit involvement — the same
/// <c>RuntimeEntityObjectLifetime.TryApplyPosition</c> call every
/// real inbound ACE broadcast makes. Its doc names this the
/// mechanism that <c>Forget</c>s any outstanding SetPosition
/// operation for the entity, including a DeferredCell park — "the
/// exact far-destination case the park exists to serve".
/// </summary>
public void MergeOrdinaryPosition(
Vector3 position, uint cellId, ushort teleportSequence)
{
_mergePositionSequence++;
var update = new WorldSession.EntityPositionUpdate(
PlayerGuid,
new CreateObject.ServerPosition(
cellId,
position.X,
position.Y,
position.Z,
1f, 0f, 0f, 0f),
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: _mergePositionSequence,
// The SAME already-accepted teleport sequence, not a
// regression - an ordinary ACE broadcast Apply during an
// active teleport still carries it unchanged; only a NEW
// portal advances it.
TeleportSequence: teleportSequence,
ForcePositionSequence: 0);
Lifetime.TryApplyPosition(
update,
isLocalPlayer: true,
forcePositionRotation: Quaternion.Identity,
currentLocalVelocity: Vector3.Zero,
acknowledgeProjection: null,
out _,
out _,
out _);
}
/// <summary>
/// Commits a SECOND landblock's collision so a cross-landblock
/// portal destination can resolve <c>Committed</c> instead of
/// parking <c>DeferredCell</c> - the constructor's own home
/// landblock is the only one committed by default.
/// </summary>
public void CommitLandblockCollision(uint landblockId)
{
var heights = new byte[81];
Array.Fill(heights, (byte)5f);
var heightTable = new float[256];
for (int i = 0; i < heightTable.Length; i++)
heightTable[i] = i;
Lifetime.Physics.SetPosition.BeginCollisionGeneration(landblockId, 1UL);
Lifetime.Physics.Engine.AddLandblock(
landblockId,
new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(),
Array.Empty<PortalPlane>(),
worldOffsetX: 0f,
worldOffsetY: 0f);
Lifetime.Physics.SetPosition.CommitCollisionGeneration(
landblockId, 1UL, ready: true);
}
/// <summary>
/// Registers a minimal synthetic indoor cell so a portal destination
/// whose low word is &gt;= 0x0100 can resolve <c>Committed</c> on its
/// FIRST placement attempt instead of parking <c>DeferredCell</c>
/// forever. <see cref="PhysicsEngine.IsSpawnCellReady"/> treats every
/// outdoor cell (low word &lt; 0x0100) as trivially ready, but an
/// indoor cell requires <c>DataCache.GetCellStruct</c> to resolve a
/// real entry - the bare fixture's <c>AddLandblock</c> calls only
/// ever pass an empty <c>CellSurface</c> list, so without this an
/// indoor destination cell is permanently un-spawnable regardless of
/// how many times collision generation is committed for its
/// landblock. Mirrors
/// <c>RuntimeSetPositionStateTests.AddSyntheticCell</c>.
/// </summary>
public void AddSyntheticIndoorCell(uint envCellId)
{
Lifetime.Physics.DataCache.RegisterCellStructForTest(
envCellId,
new CellPhysics
{
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
Portals = [new PortalInfo(0, 0, 0)],
CellBSP = new DatReaderWriter.Types.CellBSPTree
{
Root = new DatReaderWriter.Types.CellBSPNode
{
Type = BSPNodeType.Leaf,
},
},
});
Lifetime.Physics.DataCache.CellGraph.Add(
new AcDream.Core.World.Cells.EnvCell(
envCellId,
Matrix4x4.Identity,
Matrix4x4.Identity,
Vector3.Zero,
Vector3.One,
Array.Empty<AcDream.Core.World.Cells.CellPortal>(),
Array.Empty<uint>(),
seenOutside: false,
containmentBsp: null));
}
public void DrainPlacementFifo()
{
while (Lifetime.Physics.SetPosition.TryPeekProjection(
out RuntimePlacementProjectionSnapshot head))
{
if (!Lifetime.Physics.SetPosition.AcknowledgeProjection(head.Token))
break;
}
}
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell)
{
var position = new CreateObject.ServerPosition(
cell, 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,
"teleport-controller-fixture",
null,
null,
null,
PhysicsState: physics.RawState,
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
private sealed class UnusedCollisionSource : IPreparedCollisionSource
{
public PreparedAssetPresence ProbeCollision(
AcDream.Content.Pak.PakAssetType type, uint sourceFileId) =>
PreparedAssetPresence.Available;
public PreparedCollisionReadResult<FlatSetupCollision> ReadSetupCollision(
uint sourceFileId, CancellationToken cancellationToken = default) =>
PreparedCollisionReadResult<FlatSetupCollision>.Missing;
public PreparedCollisionReadResult<FlatGfxObjCollisionAsset> ReadGfxObjCollision(
uint sourceFileId, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public PreparedCollisionReadResult<FlatCellStructureCollisionAsset>
ReadCellStructureCollision(
uint sourceFileId, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public PreparedCollisionReadResult<FlatEnvCellTopology> ReadEnvCellTopology(
uint sourceFileId, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public PreparedCollisionSourceStats CollisionStats => default;
public void Dispose()
{
}
}
}
private sealed class FakeAuthority : ILocalPlayerTeleportAuthority
{
public bool IsFresh = true;
public bool IsFreshStart(ushort sequence) => IsFresh;
}
private sealed class FakeInput : ILocalPlayerTeleportInputLifetime
{
public int EndCount;
public void EndMouseLook() => EndCount++;
}
private sealed class FakeMode : ILocalPlayerTeleportModeOperations
{
private readonly List<string> _order;
public FakeMode(List<string> order)
{
_order = order;
Controller = new PlayerMovementController(new PhysicsEngine());
Controller.SeedPlacementForTest(Vector3.Zero, 0x20210001u, Vector3.Zero);
}
public PlayerMovementController? Controller { get; set; }
public Matrix4x4 Projection => Matrix4x4.Identity;
public int EnterPortalCount;
public Func<PlayerMovementController?>? RebuildOnEnter;
public bool TryEnterPortalSpace()
{
EnterPortalCount++;
Controller ??= RebuildOnEnter?.Invoke();
if (Controller is null)
return false;
Controller.State = PlayerState.PortalSpace;
return true;
}
public int EnterPortalForLoginCount;
public bool TryEnterPortalSpaceForLogin()
{
EnterPortalForLoginCount++;
return TryEnterPortalSpace();
}
public void EnterWorld()
{
_order.Add("enter-world");
if (Controller is { } controller)
controller.State = PlayerState.InWorld;
}
}
private sealed class FakeStreaming
: ILocalPlayerTeleportStreamingOperations,
IWorldRevealStreamingScheduler
{
private readonly List<string> _order;
public FakeStreaming(int centerX, int centerY, List<string> order)
{
CenterX = centerX;
CenterY = centerY;
_order = order;
}
public int CenterX { get; }
public int CenterY { get; }
public bool IsRecenterPending => RecenterPending;
public bool RecenterPending;
public bool ResetResult = true;
public bool LastResetWasSessionEnding;
public readonly List<(int X, int Y, bool Sealed)> Recenters = new();
public readonly List<(long Generation, uint Cell, int Radius)>
Reservations = new();
public readonly List<long> ReservationEnds = new();
public bool BeginRecenter(int x, int y, bool isSealedDungeon)
{
Recenters.Add((x, y, isSealedDungeon));
return !RecenterPending;
}
public bool ResetRecenter(bool sessionEnding)
{
LastResetWasSessionEnding = sessionEnding;
return ResetResult;
}
public bool IsSealedDungeon(uint cellId) =>
(cellId & 0xFFFFu) >= 0x0100u;
public void BeginDestinationReservation(
long revealGeneration,
uint destinationCell,
int requiredRenderRadius) =>
Reservations.Add(
(revealGeneration, destinationCell, requiredRenderRadius));
public void EndDestinationReservation(long revealGeneration)
{
ReservationEnds.Add(revealGeneration);
_order.Add("reservation-end");
}
}
/// <summary>
/// C4 route 3: acknowledge-only per D-T4 - no position/cell arguments
/// (the canonical Runtime commit already resolved them; this suffix only
/// runs on a COMMITTED receipt, never authors one). <see cref="Called"/>
/// stands in for the deleted <c>Position</c>/<c>CellId</c> capture: it is
/// the fact that matters now (whether the arm committed and the Place
/// edge acknowledged it), never a value this fake could plausibly
/// mis-record.
/// </summary>
private sealed class FakePlacement : ILocalPlayerTeleportPlacement
{
private readonly List<string> _order;
public FakePlacement(List<string> order) => _order = order;
public bool Called;
public Quaternion Rotation;
public Action? OnPlace;
public void Place(Quaternion rotation)
{
_order.Add("placement");
Called = true;
Rotation = rotation;
OnPlace?.Invoke();
}
}
private sealed class FakeSession : ILocalPlayerTeleportSession
{
private readonly List<string> _order;
public FakeSession(List<string> order) => _order = order;
public int LoginCompleteCount;
public Action? OnSend;
public void SendLoginComplete()
{
_order.Add("login-complete");
LoginCompleteCount++;
OnSend?.Invoke();
}
}
private sealed class FakeNetworkSink : ILocalPlayerTeleportNetworkSink
{
public List<uint> Starts { get; } = [];
public int FirstEntryCompletions;
public void OnTeleportStarted(uint sequence) => Starts.Add(sequence);
public void OfferDestination(
RuntimeTeleportDestination destination,
bool teleportTimestampAdvanced)
{
}
public void OnLocalPlayerFirstEntryCompleted() => FirstEntryCompletions++;
public void ResetSession()
{
}
public void ResetGenerationPresentation()
{
}
}
[Fact]
public void PortalCues_FireOnTheSequencersOwnSoundEvents_NotOnTheTunnelVisuals()
{
// Retail plays Sound_UI_EnterPortal as the animation BEGINS
// (gmSmartBoxUI::BeginTeleportAnimation @ 0x004D638E) and
// Sound_UI_ExitPortal at the TunnelFadeOut -> WorldFadeIn edge
// (@ 0x004D7405). The sequencer marks both with dedicated events. Hanging
// the enter cue on EnterTunnel instead — the first tunnel-family frame —
// delays it by a whole TunnelFadeIn, which is what Campaign A slice A4
// originally did.
var harness = new Harness();
harness.Controller.OnTeleportStarted(21);
harness.Presentation.Enqueue(TeleportAnimEvent.PlayEnterSound);
harness.Controller.Tick(0.016f);
Assert.Equal(["enter"], harness.Presentation.Cues);
// The tunnel becoming visible must NOT emit a second cue.
harness.Presentation.Enqueue(TeleportAnimEvent.EnterTunnel);
harness.Controller.Tick(0.016f);
Assert.Equal(["enter"], harness.Presentation.Cues);
Assert.True(harness.Presentation.IsPortalViewportVisible);
harness.Presentation.Enqueue(TeleportAnimEvent.PlayExitSound);
harness.Controller.Tick(0.016f);
Assert.Equal(["enter", "exit"], harness.Presentation.Cues);
Assert.False(harness.Presentation.IsPortalViewportVisible);
}
// ── Enter-world round (2026-08-17): the LOGIN portal-space arm ──────
//
// Retail runs the identical TAS_TUNNEL wormhole on initial login —
// SmartBox::teleport_in_progress @ 0x00451C20 goes high the moment the
// login player exists with position_update_complete == 0, and
// gmSmartBoxUI::UseTime @ 0x004D6EAB begins the same animation (and the
// same Sound_UI_EnterPortal @ 0x004D638E) it begins for an F751. These
// tests drive the login reveal (RuntimeWorldTransitState.BeginLoginReveal
// via WorldRevealCoordinator.BeginLogin — the shared edge of every entry
// route) through the controller's login pump.
[Fact]
public void LoginReveal_ArmsThePortalSpacePresentation_WithRetailCues()
{
var order = new List<string>();
var harness = new Harness(worldReady: false, order: order);
harness.Reveal.BeginLogin(0x20210001u);
Assert.Equal(RuntimePortalKind.Login, harness.Reveal.Snapshot.Kind);
// First tick: the arm claims the reveal, enters portal space, and
// begins the presentation.
harness.Controller.Tick(0.016f);
Assert.Equal(1, harness.Mode.EnterPortalCount);
Assert.Equal(Matrix4x4.Identity, harness.Presentation.BeginProjection);
Assert.Equal(0x20210001u, harness.Controller.ActiveDestinationCell);
// Enter cue at animation begin, tunnel viewport on its own edge —
// the same event contract as the F751 pump.
harness.Presentation.Enqueue(TeleportAnimEvent.PlayEnterSound);
harness.Controller.Tick(0.016f);
Assert.Equal(["enter"], harness.Presentation.Cues);
harness.Presentation.Enqueue(TeleportAnimEvent.EnterTunnel);
harness.Controller.Tick(0.016f);
Assert.True(harness.Presentation.IsPortalViewportVisible);
// Destination not ready and first placement not yet completed: the
// sequencer must keep seeing worldReady == false (its Tunnel hold).
Assert.All(harness.Presentation.WorldReadyValues, value => Assert.False(value));
Assert.Equal(0, harness.Session.LoginCompleteCount);
// Readiness alone is not enough — the first-entry conductor's
// canonical placement is half of the latch.
harness.WorldReady = true;
harness.Controller.Tick(0.016f);
Assert.False(harness.Presentation.WorldReadyValues[^1]);
harness.Controller.OnLocalPlayerFirstEntryCompleted();
harness.Controller.Tick(0.016f);
Assert.True(harness.Presentation.WorldReadyValues[^1]);
// The login pump has no Place edge of its own: the conductor already
// placed the player (retail: SmartBox::UseTime @ 0x00455483 only
// flips position_update_complete).
harness.Presentation.Enqueue(TeleportAnimEvent.Place);
harness.Controller.Tick(0.016f);
Assert.False(harness.Placement.Called);
// Viewport swap: reservation release + exit cue + tunnel retire
// (gmSmartBoxUI::UseTime, Sound_UI_ExitPortal @ 0x004D7405).
harness.Presentation.Enqueue(TeleportAnimEvent.PlayExitSound);
harness.Controller.Tick(0.016f);
Assert.Equal(["enter", "exit"], harness.Presentation.Cues);
Assert.False(harness.Presentation.IsPortalViewportVisible);
Assert.Single(harness.Streaming.ReservationEnds);
// WorldFadeIn end: EnterWorld + the ONE LoginComplete + reveal
// completion (gmSmartBoxUI::UseTime @ 0x004D745D).
harness.Presentation.Enqueue(TeleportAnimEvent.FireLoginComplete);
harness.Controller.Tick(0.016f);
Assert.Contains("enter-world", order);
Assert.Equal(1, harness.Session.LoginCompleteCount);
Assert.True(harness.Reveal.Snapshot.Completed);
Assert.Equal(0u, harness.Controller.ActiveDestinationCell);
// The completed reveal stays completed; no re-arm, no second send.
harness.Controller.Tick(0.016f);
Assert.Equal(1, harness.Mode.EnterPortalCount);
Assert.Equal(1, harness.Session.LoginCompleteCount);
}
[Fact]
public void LoginPump_SendsLoginCompleteOnlyAtThePresentationEnd()
{
var harness = new Harness(worldReady: true);
harness.Reveal.BeginLogin(0x20210001u);
harness.Controller.OnLocalPlayerFirstEntryCompleted();
// Ticks before the FireLoginComplete edge never send.
for (int i = 0; i < 5; i++)
harness.Controller.Tick(0.016f);
Assert.Equal(0, harness.Session.LoginCompleteCount);
harness.Presentation.Enqueue(TeleportAnimEvent.FireLoginComplete);
harness.Controller.Tick(0.016f);
Assert.Equal(1, harness.Session.LoginCompleteCount);
Assert.True(harness.Reveal.Snapshot.Completed);
}
[Fact]
public void RealTeleportStart_SupersedesTheLoginPresentation()
{
var order = new List<string>();
var harness = new Harness(worldReady: true, order: order);
harness.Reveal.BeginLogin(0x20210001u);
harness.Controller.OnLocalPlayerFirstEntryCompleted();
harness.Controller.Tick(0.016f);
harness.Presentation.Enqueue(TeleportAnimEvent.EnterTunnel);
harness.Controller.Tick(0.016f);
Assert.True(harness.Presentation.IsPortalViewportVisible);
// An F751 mid-login-tunnel (ACE can relocate a broken spawn at
// login): the portal pump takes the presentation over and owns the
// single LoginComplete, mirroring retail's one teleportInProgress
// flag spanning both.
harness.Controller.OnTeleportStarted(3);
Assert.Contains("presentation-reset", order);
Assert.True(harness.Controller.IsActive);
Assert.Equal(0, harness.Session.LoginCompleteCount);
// The login claim is gone: ticking the portal-active controller
// never re-enters the login pump (no second presentation-begin from
// the login arm beyond the teleport activation's own).
int loginCompletes = harness.Session.LoginCompleteCount;
harness.Controller.Tick(0.016f);
Assert.Equal(loginCompletes, harness.Session.LoginCompleteCount);
}
private sealed class FakePresentation : ILocalPlayerTeleportPresentation
{
private readonly List<string> _order;
private readonly Queue<IReadOnlyList<TeleportAnimEvent>> _events = new();
public FakePresentation(List<string> order) => _order = order;
public bool IsPortalViewportVisible { get; private set; }
public int CurrentTunnelFrame => 72;
public Matrix4x4? BeginProjection;
public bool EmitPlaceWhenReady;
public readonly List<bool> WorldReadyValues = new();
public readonly List<bool> WaitCueValues = new();
public int DisposeFailuresRemaining;
public int DisposeCount;
public void Enqueue(params TeleportAnimEvent[] events) => _events.Enqueue(events);
public void Begin(Matrix4x4 projection)
{
BeginProjection = projection;
_order.Add("presentation-begin");
}
public (TeleportAnimSnapshot Snapshot, IReadOnlyList<TeleportAnimEvent> Events)
Tick(float deltaSeconds, bool worldReady)
{
WorldReadyValues.Add(worldReady);
if (_events.Count > 0)
return (default, _events.Dequeue());
if (EmitPlaceWhenReady && worldReady)
{
EmitPlaceWhenReady = false;
return (default, new[] { TeleportAnimEvent.Place });
}
return (default, Array.Empty<TeleportAnimEvent>());
}
public void TickTunnel(float deltaSeconds) => _order.Add("tunnel-tick");
public readonly List<string> Cues = new();
public void PlayEnterCue() => Cues.Add("enter");
public void PlayExitCue() => Cues.Add("exit");
public void EnterTunnel() => IsPortalViewportVisible = true;
public void ExitTunnel() => IsPortalViewportVisible = false;
public void SetWaitCue(bool visible) => WaitCueValues.Add(visible);
public void Reset()
{
_order.Add("presentation-reset");
IsPortalViewportVisible = false;
BeginProjection = null;
_events.Clear();
}
public Matrix4x4 ApplyViewPlane(Matrix4x4 projection) => projection;
public ICamera ApplyViewPlane(ICamera camera) => camera;
public void DrawPortalViewport(int width, int height, Matrix4x4 projection) { }
public void Dispose()
{
DisposeCount++;
if (DisposeFailuresRemaining-- > 0)
throw new InvalidOperationException("injected disposal failure");
}
}
}