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:
parent
302d90209d
commit
5ca1d47d7a
7 changed files with 533 additions and 16 deletions
|
|
@ -141,13 +141,39 @@ internal sealed class PlayerModeController :
|
|||
if (Controller is null && !TryEnter("teleport"))
|
||||
return false;
|
||||
|
||||
if (Controller is not { } controller)
|
||||
// Enter-world round (2026-08-17): the Runtime first-entry conductor
|
||||
// can have CONSTRUCTED the controller without publishing it yet
|
||||
// (CandidatePreparing/Sealed/Dormant). A State write in that window
|
||||
// throws (EnsurePublishedForRuntimeOperation); CanExecuteLiveMovement
|
||||
// is the documented skip-instead-of-fault predicate for exactly this
|
||||
// pre-publication login window. Refuse — every caller retries on its
|
||||
// own tick cadence.
|
||||
if (Controller is not { CanExecuteLiveMovement: true } controller)
|
||||
return false;
|
||||
|
||||
controller.State = PlayerState.PortalSpace;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enter-world round (2026-08-17): the login tunnel's portal-space entry.
|
||||
/// The login wormhole begins before any player-mode entry has happened
|
||||
/// (retail's camera set simply follows its always-present player), so
|
||||
/// this first performs the SAME presentation attach the post-reveal
|
||||
/// auto-entry used to run (<see cref="TryEnter"/> — chase camera, shadow,
|
||||
/// animation sinks, approach lifetime), then flips into portal space.
|
||||
/// <see cref="TryEnter"/> itself refuses until the Runtime first-entry
|
||||
/// conductor commits, so pre-publication calls fail closed and retry.
|
||||
/// </summary>
|
||||
public bool TryEnterPortalSpaceForLogin()
|
||||
{
|
||||
_autoEntry?.Cancel();
|
||||
if (!_mode.IsPlayerMode && !TryEnter("login"))
|
||||
return false;
|
||||
|
||||
return TryEnterPortalSpace();
|
||||
}
|
||||
|
||||
public void EnterWorld()
|
||||
{
|
||||
if (Controller is { } controller)
|
||||
|
|
|
|||
|
|
@ -337,16 +337,25 @@ internal sealed class LiveSessionRuntimeFactory
|
|||
_world.FirstEntryDrive,
|
||||
_ =>
|
||||
{
|
||||
session.SendGameAction(GameActionLoginComplete.Build());
|
||||
// Enter-world round (2026-08-17): the graphical host no
|
||||
// longer sends LoginComplete here. Retail sends 0xA1 at the
|
||||
// END of the login wormhole (gmSmartBoxUI::UseTime
|
||||
// @0x004D745D -> CPlayerSystem::SendLoginCompleteNotification
|
||||
// @0x00562E90), and the login portal-space presentation now
|
||||
// runs on this host, so the send rides its
|
||||
// FireLoginComplete edge (LocalPlayerTeleportController's
|
||||
// login pump) — the same edge the F751 pump already uses.
|
||||
// This notification is the presentation's worldReady latch:
|
||||
// the canonical local-player first placement committed
|
||||
// (retail's position_update_complete=1 analogue,
|
||||
// SmartBox::UseTime @0x00455483).
|
||||
_world.Teleport.OnLocalPlayerFirstEntryCompleted();
|
||||
// Night-round review F2: CM_House::Event_QueryHouse @0x006aaa00
|
||||
// is tail-called, unconditionally, from the end of
|
||||
// CPlayerSystem::InitializePlayer @0x00563570 — the SAME
|
||||
// once-per-session function AttemptSendLoginCompleteNotification
|
||||
// lives in (guarded by player_initialized), right after that
|
||||
// notification. This is the graphical host's direct
|
||||
// (non-portal) first-entry completion edge — the exact
|
||||
// analogue. Portal-space re-entries (LocalPlayerTeleportController)
|
||||
// do NOT resend it, matching retail's single-shot guard.
|
||||
// CPlayerSystem::InitializePlayer @0x00563570 (guarded by
|
||||
// player_initialized, once per session) — an object-arrival
|
||||
// edge, NOT a tunnel edge, so it stays at first-entry
|
||||
// completion rather than moving with LoginComplete.
|
||||
session.SendHouseQuery();
|
||||
},
|
||||
_world.AcceptedPositionDrive,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,21 @@ internal interface ILocalPlayerTeleportNetworkSink
|
|||
RuntimeTeleportDestination destination,
|
||||
bool teleportTimestampAdvanced);
|
||||
|
||||
/// <summary>
|
||||
/// Enter-world round (2026-08-17): the graphical host's local-player
|
||||
/// first-entry conductor completed its canonical initial placement.
|
||||
/// This is the login analogue of retail's
|
||||
/// <c>SmartBox::UseTime @ 0x00455410</c> setting
|
||||
/// <c>position_update_complete = 1</c> — the fact that lets the login
|
||||
/// portal-space presentation leave its Tunnel hold. LoginComplete
|
||||
/// (0xA1) itself now rides the presentation's own
|
||||
/// <see cref="TeleportAnimEvent.FireLoginComplete"/> edge, matching
|
||||
/// retail's send at the WorldFadeIn end
|
||||
/// (<c>gmSmartBoxUI::UseTime @ 0x004D745D</c> →
|
||||
/// <c>CPlayerSystem::SendLoginCompleteNotification @ 0x00562E90</c>).
|
||||
/// </summary>
|
||||
void OnLocalPlayerFirstEntryCompleted();
|
||||
|
||||
void ResetSession();
|
||||
|
||||
void ResetGenerationPresentation();
|
||||
|
|
@ -65,6 +80,9 @@ internal sealed class DeferredLocalPlayerTeleportNetworkSink
|
|||
bool teleportTimestampAdvanced) =>
|
||||
Required().OfferDestination(destination, teleportTimestampAdvanced);
|
||||
|
||||
public void OnLocalPlayerFirstEntryCompleted() =>
|
||||
Required().OnLocalPlayerFirstEntryCompleted();
|
||||
|
||||
public void ResetSession() => Required().ResetSession();
|
||||
|
||||
public void ResetGenerationPresentation() =>
|
||||
|
|
@ -102,6 +120,19 @@ internal interface ILocalPlayerTeleportModeOperations
|
|||
PlayerMovementController? Controller { get; }
|
||||
Matrix4x4 Projection { get; }
|
||||
bool TryEnterPortalSpace();
|
||||
|
||||
/// <summary>
|
||||
/// Enter-world round (2026-08-17): the LOGIN arm's portal-space entry.
|
||||
/// Unlike <see cref="TryEnterPortalSpace"/> — whose F751 callers run when
|
||||
/// player-mode presentation is already attached (or deliberately absent,
|
||||
/// e.g. fly mode) — the login tunnel begins BEFORE any player-mode entry
|
||||
/// has happened, so this operation must also perform the same
|
||||
/// presentation attach the post-reveal auto-entry used to do (chase
|
||||
/// camera, shadow, animation sinks) before flipping the controller into
|
||||
/// portal space. Refuses (retryable) until the Runtime first-entry
|
||||
/// conductor has published the movement controller.
|
||||
/// </summary>
|
||||
bool TryEnterPortalSpaceForLogin();
|
||||
void EnterWorld();
|
||||
}
|
||||
|
||||
|
|
@ -449,6 +480,37 @@ internal sealed class LocalPlayerTeleportController
|
|||
private long _lifetimeGeneration;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Enter-world round (2026-08-17): the login reveal generation this
|
||||
/// controller's presentation currently owns (0 = none). Retail runs the
|
||||
/// SAME wormhole machine at initial login as at an F751 teleport —
|
||||
/// <c>SmartBox::teleport_in_progress @ 0x00451C20</c> returns 1 whenever
|
||||
/// the SmartBox has a player whose <c>position_update_complete</c> is
|
||||
/// still 0, which is true the moment the login CreatePlayer lands, and
|
||||
/// <c>gmSmartBoxUI::UseTime @ 0x004D6EAB</c> edge-detects that flag into
|
||||
/// <c>BeginTeleportAnimation(TAS_TUNNEL) @ 0x004D6EC9</c> (which plays
|
||||
/// <c>Sound_UI_EnterPortal @ 0x004D638E</c>) with no F751 involved.
|
||||
/// acdream's canonical equivalent of that condition is Runtime's login
|
||||
/// reveal (<see cref="RuntimeWorldTransitState.BeginLoginReveal"/>,
|
||||
/// begun on the first accepted local-player position) — so this arm keys
|
||||
/// the same presentation off that Runtime-owned lifecycle instead of a
|
||||
/// teleport start.
|
||||
/// </summary>
|
||||
private long _loginRevealGeneration;
|
||||
private bool _loginPresentationActive;
|
||||
|
||||
/// <summary>
|
||||
/// Latched by <see cref="OnLocalPlayerFirstEntryCompleted"/> — the
|
||||
/// first-entry conductor's canonical initial placement committed. The
|
||||
/// login pump's <c>worldReady</c> requires it, so the sequencer cannot
|
||||
/// leave its Tunnel hold (and therefore cannot reach
|
||||
/// <see cref="TeleportAnimEvent.FireLoginComplete"/>) before the same
|
||||
/// placement contract that previously gated the immediate LoginComplete
|
||||
/// send. Session-scoped: cleared only by session-level resets.
|
||||
/// </summary>
|
||||
private bool _loginPlacementCompleted;
|
||||
private float _loginHoldSeconds;
|
||||
|
||||
public LocalPlayerTeleportController(
|
||||
ILocalPlayerTeleportAuthority authority,
|
||||
ILocalPlayerTeleportInputLifetime input,
|
||||
|
|
@ -476,8 +538,35 @@ internal sealed class LocalPlayerTeleportController
|
|||
|
||||
public bool IsActive => _transit.IsTeleportActive;
|
||||
public bool IsPortalViewportVisible => _presentation.IsPortalViewportVisible;
|
||||
public uint ActiveDestinationCell =>
|
||||
_transit.IsTeleportActive ? _pendingCell : 0u;
|
||||
|
||||
/// <summary>
|
||||
/// The destination the portal viewport is currently holding for. The
|
||||
/// render frame's reveal-preparation arm
|
||||
/// (<see cref="AcDream.App.Rendering.RuntimeRenderFrameLivePreparation"/>)
|
||||
/// keys composite-texture preparation and readiness evaluation off this
|
||||
/// cell; the login presentation must report its own destination here
|
||||
/// because entering portal space flips <c>ChaseModeEverEntered</c>, which
|
||||
/// retires the fallback "waiting for login" cell source that used to keep
|
||||
/// preparation running pre-entry.
|
||||
/// </summary>
|
||||
public uint ActiveDestinationCell
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_transit.IsTeleportActive)
|
||||
return _pendingCell;
|
||||
if (_loginPresentationActive)
|
||||
{
|
||||
RuntimePortalSnapshot snapshot = _transit.Snapshot;
|
||||
if (snapshot.Kind == RuntimePortalKind.Login
|
||||
&& snapshot.Generation == _loginRevealGeneration)
|
||||
{
|
||||
return snapshot.Readiness.DestinationCell;
|
||||
}
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnTeleportStarted(uint sequence)
|
||||
{
|
||||
|
|
@ -515,13 +604,22 @@ internal sealed class LocalPlayerTeleportController
|
|||
TryAimAcceptedDestination();
|
||||
}
|
||||
|
||||
public void OnLocalPlayerFirstEntryCompleted()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
_loginPlacementCompleted = true;
|
||||
}
|
||||
|
||||
public void Tick(float deltaSeconds)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
TryActivatePendingPresentation();
|
||||
TryAimAcceptedDestination();
|
||||
if (!_transit.IsTeleportActive)
|
||||
{
|
||||
TickLoginPresentation(deltaSeconds);
|
||||
return;
|
||||
}
|
||||
|
||||
long generation = _lifetimeGeneration;
|
||||
ushort sequence = _transit.ActiveTeleportSequence;
|
||||
|
|
@ -902,6 +1000,225 @@ internal sealed class LocalPlayerTeleportController
|
|||
+ $"(seq={_transit.ActiveTeleportSequence})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enter-world round (2026-08-17): the login half of retail's ONE
|
||||
/// wormhole machine. Retail begins the identical TAS_TUNNEL animation for
|
||||
/// initial login and for F751 teleports from the same
|
||||
/// <c>gmSmartBoxUI::UseTime @ 0x004D6EAB</c> flag edge —
|
||||
/// <c>SmartBox::teleport_in_progress @ 0x00451C20</c> goes high the
|
||||
/// moment the login player object exists with
|
||||
/// <c>position_update_complete == 0</c>, no F751 required. acdream's
|
||||
/// canonical login edge is Runtime's login reveal generation
|
||||
/// (<see cref="RuntimeWorldTransitState.BeginLoginReveal"/>, begun on the
|
||||
/// first accepted local-player position on every entry route: direct
|
||||
/// auto-select, character-select Enter, and enter-after-create).
|
||||
///
|
||||
/// <para>
|
||||
/// Activation retries every Tick until
|
||||
/// <see cref="ILocalPlayerTeleportModeOperations.TryEnterPortalSpace"/>
|
||||
/// succeeds — the same retry shape the F751 arm uses — which requires the
|
||||
/// Runtime first-entry conductor's published movement controller.
|
||||
/// Entering portal space cancels the player-mode auto-entry (its first
|
||||
/// statement), so this arm owns the reveal's
|
||||
/// EnterWorld/LoginComplete/Complete suffix exactly like the teleport
|
||||
/// pump owns its own; the update-frame order (teleport phase before
|
||||
/// auto-entry) guarantees this claim happens before auto-entry could
|
||||
/// fire.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private void TryActivateLoginPresentation()
|
||||
{
|
||||
if (_transit.HasPendingTeleportStart || _transit.IsTeleportActive)
|
||||
return;
|
||||
|
||||
RuntimePortalSnapshot snapshot = _transit.Snapshot;
|
||||
if (snapshot.Kind != RuntimePortalKind.Login
|
||||
|| snapshot.Generation == 0
|
||||
|| snapshot.Completed
|
||||
|| snapshot.Cancelled
|
||||
|| _loginRevealGeneration == snapshot.Generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Quiet pre-gate: until the first-entry conductor PUBLISHES the
|
||||
// movement controller, portal-space entry cannot succeed (and the
|
||||
// full entry path would log a refusal every tick). Retry silently.
|
||||
if (_mode.Controller is not { CanExecuteLiveMovement: true })
|
||||
return;
|
||||
|
||||
long generation = _lifetimeGeneration;
|
||||
if (!_mode.TryEnterPortalSpaceForLogin()
|
||||
|| _lifetimeGeneration != generation
|
||||
|| _mode.Controller is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-read after the mode entry: TryEnterPortalSpaceForLogin can run
|
||||
// arbitrary presentation attach work.
|
||||
snapshot = _transit.Snapshot;
|
||||
if (snapshot.Kind != RuntimePortalKind.Login
|
||||
|| snapshot.Generation == 0
|
||||
|| snapshot.Completed
|
||||
|| snapshot.Cancelled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_loginRevealGeneration = snapshot.Generation;
|
||||
_loginPresentationActive = true;
|
||||
_loginHoldSeconds = 0f;
|
||||
_presentation.Begin(_mode.Projection);
|
||||
Console.WriteLine(
|
||||
$"live: login portal-space presentation started "
|
||||
+ $"(gen={snapshot.Generation} "
|
||||
+ $"cell=0x{snapshot.Readiness.DestinationCell:X8})");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame pump for the login presentation — the login mirror of the
|
||||
/// teleport pump in <see cref="Tick"/>. Differences, each anchored in
|
||||
/// retail: there is no Place edge to drive (the first-entry conductor
|
||||
/// committed the canonical placement before this presentation could
|
||||
/// begin — retail's <c>SmartBox::UseTime @ 0x00455483</c> likewise only
|
||||
/// flips <c>position_update_complete</c>, it does not place), and
|
||||
/// LoginComplete rides <see cref="TeleportAnimEvent.FireLoginComplete"/>
|
||||
/// at the WorldFadeIn end (<c>gmSmartBoxUI::UseTime @ 0x004D745D</c> →
|
||||
/// <c>CPlayerSystem::SendLoginCompleteNotification @ 0x00562E90</c>)
|
||||
/// instead of at raw first placement.
|
||||
/// </summary>
|
||||
private void TickLoginPresentation(float deltaSeconds)
|
||||
{
|
||||
TryActivateLoginPresentation();
|
||||
|
||||
RuntimePortalSnapshot snapshot = _transit.Snapshot;
|
||||
bool revealActive = snapshot.Kind == RuntimePortalKind.Login
|
||||
&& snapshot.Generation != 0
|
||||
&& !snapshot.Completed
|
||||
&& !snapshot.Cancelled;
|
||||
if (!revealActive || _loginRevealGeneration != snapshot.Generation)
|
||||
{
|
||||
if (_loginPresentationActive)
|
||||
{
|
||||
// The reveal this presentation was serving ended underneath
|
||||
// it (cancel, supersession, or session reset that did not
|
||||
// route through this controller's own reset). Drop the claim
|
||||
// and retire the visuals; a successor reveal re-activates
|
||||
// through TryActivateLoginPresentation above.
|
||||
_loginRevealGeneration = 0;
|
||||
_loginPresentationActive = false;
|
||||
_loginHoldSeconds = 0f;
|
||||
_presentation.Reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_loginPresentationActive)
|
||||
return;
|
||||
|
||||
long generation = _lifetimeGeneration;
|
||||
long revealGeneration = snapshot.Generation;
|
||||
uint destinationCell = snapshot.Readiness.DestinationCell;
|
||||
|
||||
bool originReady = !_streaming.IsRecenterPending;
|
||||
bool worldReady = _loginPlacementCompleted
|
||||
&& originReady
|
||||
&& _worldReveal.Evaluate(destinationCell).IsReady;
|
||||
if (!IsCurrentLoginLifetime(generation, revealGeneration))
|
||||
return;
|
||||
|
||||
if (!worldReady)
|
||||
_loginHoldSeconds += deltaSeconds;
|
||||
_presentation.SetWaitCue(
|
||||
!worldReady
|
||||
&& _worldReveal.ObserveWait(
|
||||
TimeSpan.FromSeconds(_loginHoldSeconds)));
|
||||
|
||||
var (_, events) = _presentation.Tick(deltaSeconds, worldReady);
|
||||
if (!IsCurrentLoginLifetime(generation, revealGeneration))
|
||||
return;
|
||||
|
||||
foreach (TeleportAnimEvent teleportEvent in events)
|
||||
{
|
||||
switch (teleportEvent)
|
||||
{
|
||||
case TeleportAnimEvent.PlayEnterSound:
|
||||
// Sound_UI_EnterPortal as the animation begins —
|
||||
// BeginTeleportAnimation @ 0x004D638E, identical for the
|
||||
// login entry. Logged (once per login) as the audio-start
|
||||
// evidence line for connected gates.
|
||||
Console.WriteLine(
|
||||
"live: login portal-space enter cue "
|
||||
+ "(Sound_UI_EnterPortal)");
|
||||
_presentation.PlayEnterCue();
|
||||
if (!IsCurrentLoginLifetime(generation, revealGeneration))
|
||||
return;
|
||||
break;
|
||||
case TeleportAnimEvent.EnterTunnel:
|
||||
_presentation.EnterTunnel();
|
||||
if (!IsCurrentLoginLifetime(generation, revealGeneration))
|
||||
return;
|
||||
break;
|
||||
case TeleportAnimEvent.Place:
|
||||
// No login Place edge: the canonical initial placement is
|
||||
// the first-entry conductor's, already committed (the
|
||||
// worldReady latch above requires it). Retail's login
|
||||
// analogue only flips position_update_complete
|
||||
// (SmartBox::UseTime @ 0x00455483).
|
||||
break;
|
||||
case TeleportAnimEvent.PlayExitSound:
|
||||
// Release destination cell blocking at the exact
|
||||
// portal/world viewport swap — same edge as the teleport
|
||||
// pump (gmSmartBoxUI::UseTime @ 0x004D6E30), with
|
||||
// Sound_UI_ExitPortal @ 0x004D7405.
|
||||
_worldReveal.RevealWorldViewport();
|
||||
if (!IsCurrentLoginLifetime(generation, revealGeneration))
|
||||
return;
|
||||
_presentation.PlayExitCue();
|
||||
if (!IsCurrentLoginLifetime(generation, revealGeneration))
|
||||
return;
|
||||
_presentation.ExitTunnel();
|
||||
if (!IsCurrentLoginLifetime(generation, revealGeneration))
|
||||
return;
|
||||
break;
|
||||
case TeleportAnimEvent.FireLoginComplete:
|
||||
_mode.EnterWorld();
|
||||
if (!IsCurrentLoginLifetime(generation, revealGeneration))
|
||||
return;
|
||||
_session.SendLoginComplete();
|
||||
if (!IsCurrentLoginLifetime(generation, revealGeneration))
|
||||
return;
|
||||
_worldReveal.Complete();
|
||||
_loginRevealGeneration = 0;
|
||||
_loginPresentationActive = false;
|
||||
_loginHoldSeconds = 0f;
|
||||
Console.WriteLine(
|
||||
"live: login portal-space presentation complete");
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_presentation.TickTunnel(deltaSeconds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The login pump's currency check — the login mirror of
|
||||
/// <see cref="IsCurrentLifetime(long, ushort)"/>: same controller
|
||||
/// lifetime, still no active teleport (an F751 supersedes the login
|
||||
/// presentation), and the transit snapshot still carries the exact
|
||||
/// claimed login reveal generation.
|
||||
/// </summary>
|
||||
private bool IsCurrentLoginLifetime(
|
||||
long lifetimeGeneration,
|
||||
long revealGeneration) =>
|
||||
_lifetimeGeneration == lifetimeGeneration
|
||||
&& !_transit.IsTeleportActive
|
||||
&& _loginRevealGeneration == revealGeneration
|
||||
&& _transit.Snapshot.Generation == revealGeneration;
|
||||
|
||||
private void TryAimAcceptedDestination()
|
||||
{
|
||||
if (!_transit.TryGetAcceptedTeleportDestination(
|
||||
|
|
@ -1030,6 +1347,18 @@ internal sealed class LocalPlayerTeleportController
|
|||
_placementCommitted = false;
|
||||
_awaitingDeferredWake = false;
|
||||
_holdSeconds = 0f;
|
||||
// Enter-world round (2026-08-17): an F751 arriving mid-login-tunnel
|
||||
// (clearSession: false) withdraws the login presentation's claim —
|
||||
// the portal pump supersedes it and owns the single LoginComplete,
|
||||
// exactly as retail's one teleportInProgress flag stays high across
|
||||
// both and sends once at the eventual WorldFadeIn end. The
|
||||
// placement-completed latch is a session fact: it survives the
|
||||
// teleport-scoped reset and clears only with the session.
|
||||
_loginRevealGeneration = 0;
|
||||
_loginPresentationActive = false;
|
||||
_loginHoldSeconds = 0f;
|
||||
if (clearSession)
|
||||
_loginPlacementCompleted = false;
|
||||
|
||||
_streaming.ResetRecenter(clearSession);
|
||||
if (_lifetimeGeneration != generation)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue