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:
commit
2f8c046aba
9 changed files with 568 additions and 17 deletions
|
|
@ -1825,6 +1825,8 @@ public sealed class LiveEntityNetworkOnPositionCollapseMatrixTests
|
|||
bool teleportTimestampAdvanced)
|
||||
{ }
|
||||
|
||||
public void OnLocalPlayerFirstEntryCompleted() { }
|
||||
|
||||
public void ResetSession() { }
|
||||
|
||||
public void ResetGenerationPresentation() { }
|
||||
|
|
|
|||
|
|
@ -1059,6 +1059,8 @@ public sealed class LiveEntityNetworkRemoteTeleportPresentationTests
|
|||
bool teleportTimestampAdvanced)
|
||||
{ }
|
||||
|
||||
public void OnLocalPlayerFirstEntryCompleted() { }
|
||||
|
||||
public void ResetSession() { }
|
||||
|
||||
public void ResetGenerationPresentation() { }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue