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>
This commit is contained in:
Erik 2026-08-17 10:51:33 +02:00
parent 302d90209d
commit 5ca1d47d7a
7 changed files with 533 additions and 16 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;