Merge campaign-enter-portal into the round branch: login portal-space presentation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-17 11:21:46 +02:00
commit 2f8c046aba
9 changed files with 568 additions and 17 deletions

View file

@ -1825,6 +1825,8 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
bool teleportTimestampAdvanced)
{ }
public void OnLocalPlayerFirstEntryCompleted() { }
public void ResetSession() { }
public void ResetGenerationPresentation() { }

View file

@ -1059,6 +1059,8 @@ public sealed class LiveEntityNetworkRemoteTeleportPresentationTests
bool teleportTimestampAdvanced)
{ }
public void OnLocalPlayerFirstEntryCompleted() { }
public void ResetSession() { }
public void ResetGenerationPresentation() { }

View file

@ -911,12 +911,22 @@ public sealed class LocalPlayerTeleportControllerTests
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);
@ -927,10 +937,10 @@ public sealed class LocalPlayerTeleportControllerTests
Reveal = new WorldRevealCoordinator(
Transit,
revealWindow: () => RevealWindow,
isRenderNeighborhoodReady: (_, _, _) => worldReady,
isSpawnCellReady: _ => worldReady,
isTerrainNeighborhoodReady: (_, _) => worldReady,
areCompositeTexturesReady: () => worldReady,
isRenderNeighborhoodReady: (_, _, _) => WorldReady,
isSpawnCellReady: _ => WorldReady,
isTerrainNeighborhoodReady: (_, _) => WorldReady,
areCompositeTexturesReady: () => WorldReady,
prepareCompositeTextures: (_, _) => { },
invalidateCompositeTextures: () => { },
isSpawnClaimUnhydratable: _ => false,
@ -1330,6 +1340,14 @@ public sealed class LocalPlayerTeleportControllerTests
return true;
}
public int EnterPortalForLoginCount;
public bool TryEnterPortalSpaceForLogin()
{
EnterPortalForLoginCount++;
return TryEnterPortalSpace();
}
public void EnterWorld()
{
_order.Add("enter-world");
@ -1439,12 +1457,14 @@ public sealed class LocalPlayerTeleportControllerTests
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()
{
}
@ -1482,6 +1502,135 @@ public sealed class LocalPlayerTeleportControllerTests
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;