feat(session): the in-world logoff — LogOut animation, reverse wormhole, live return to character select
Retires AD-74 (Exit to Character Selection 'behaves as Exit Game') and files AD-110 (the composed handoff edge) — register rows in this commit. Retail derivation (named decomp): - gmGamePlayUI::UseTime @0x004EA3A0: confirmed Yes drains into CPlayerSystem::LogOffCharacter(0) when grounded (transient_state & CONTACT); the grounded three-way branch now also covers the indicator-bar end-session control (it was Options-only). - CPlayerSystem::LogOffCharacter @0x00563520: SaveToServer FIRST (the existing pre-logoff flush hook), then RequestLogOff @0x00562DD0: 'Logging off...' chat (type 0), 0xF653 via Proto_UI::LogOffCharacter @0x00546A20, logOffRequestTime = now + 3.0 (+20.0 when IsPlayerKiller @0x0058C910 — PWD bits 0x20|0x2000000), and CommandInterpreter::HandleLogOff @0x006B3330 -> Disable. - The log-off ANIMATION is server-driven: ACE broadcasts MotionCommand.LogOut (0x1000011E, Player.cs:596 SendMotionAsCommands) and it plays on the local player through the existing inbound unpack_movement funnel during the 3 s hold — retail plays nothing locally; Disable() is the whole client-side effect. - gmSmartBoxUI::UseTime @0x004D6E64: hold elapsed -> BeginTeleportAnimation(TAS_WORLD_FADE_OUT) @0x004D6E83 (enter cue @0x004D638E, unconditional) -> TunnelFadeIn -> Tunnel. The tunnel plays the SAME forward 40 fps animation; nothing renders backwards, and NO exit cue ever fires on logout (the char-select swap preempts the TunnelContinue/FadeOut tail). - Inbound 0xF653 echo (dispatch case 3 @0x0055C963) -> ExecuteLogOff @0x0055D780: world teardown with the LOGON CONNECTION KEPT (ExitWorldDisconnect @0x00541E00 removes every connection except logonRecID_ — one connection against ACE) and Proto_UI::SetEventCounter(0) @0x00541E79; the fresh CharacterList in the same batch re-shows character management (gmGamePlayUI::Update @0x004E9CD0 -> QueueUIMode(0x1000000a)). ACE mirrors it: SendFinalLogOffMessages (Session.cs:249) sends 0xF653 + CharacterList + ServerName >=6 s after the request and leaves the session AuthConnected — a second EnterWorld needs no re-handshake. Implementation: - RuntimeWorldTransitState: the canonical logout lifecycle (Requested/PresentationActive/Confirmed, retail 3 s/+20 s holds, cancel/reset/ownership convergence). - WorldSession: RequestCharacterLogOff (non-blocking 0xF653), IsCharacterLogOffConfirmed, ReturnToCharacterSelect (InWorld -> InCharacterSelect + game-action sequence reset; transport untouched). - LiveSessionController: BeginCharacterLogOff (flush-first request) and CompleteCharacterLogOff — the return-to-selection transaction (ReconnectCore minus the transport swap: retire the world generation's routes, host reset, state flip, fresh generation re-bind, roster re-applied from the pushed CharacterList; failures degrade to the full StopCore teardown). - RuntimeLocalPlayerMovementState.DisableCommandInterpreter + DispatcherMovementInputSource gate: retail's Disable() — held keys produce no movement while the server LogOut motion plays; cleared by the generation reset. - LocalPlayerTeleportController: the logout pump as the third arm of the one wormhole machine (request/hold/wormhole/confirmed handoff; teleport starts refused during logout; the handoff runs the session transaction whose world reset retires the tunnel as the fresh selection state re-shows the character screen). - UI: both end-session surfaces share the retail three-way grounded gate and now run the REAL flow; Options' Exit Game keeps the app exit (window close -> the existing graceful-shutdown logoff). Tests: +5 transit lifecycle, +4 session transaction, +7 logout pump. Runtime 1756/0 (baseline 1747), App live-DAT 5523/3 (baseline 5512/3 + 11 this round), Core.Net 1004/0, full solution green (0 failures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2bc81480d4
commit
d233f81dce
17 changed files with 1399 additions and 37 deletions
|
|
@ -117,7 +117,15 @@ public sealed record IndicatorRuntimeBindings(
|
|||
Func<LinkStatusSnapshot> LinkStatus,
|
||||
Func<double> CurrentTime,
|
||||
Action RequestLinkStatusPing,
|
||||
Action EndCharacterSession);
|
||||
/// <summary>Logout round (2026-08-17): the IN-WORLD character logoff —
|
||||
/// retail's end-character-session flow (log-off animation, reverse
|
||||
/// wormhole, return to character select on the live connection). Was
|
||||
/// the window-close action before the logout flow existed.</summary>
|
||||
Action EndCharacterSession,
|
||||
/// <summary>The app-exit action (window close → the graceful-shutdown
|
||||
/// logoff in WorldSession.Dispose) — the Options panel's Exit Game
|
||||
/// button, retail's m_shouldQuitOnLogout arm.</summary>
|
||||
Action ExitGame);
|
||||
|
||||
public sealed record ToolbarRuntimeBindings(
|
||||
ClientObjectTable Objects,
|
||||
|
|
@ -2461,18 +2469,52 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
|
||||
private void RequestEndCharacterSession()
|
||||
{
|
||||
ShowConfirmation(ResolveEndCharacterSessionConfirmMessage(), accepted =>
|
||||
// Logout round (2026-08-17): retail funnels BOTH end-session
|
||||
// surfaces (the indicator bar's control and the Options panel's
|
||||
// Exit to Character Selection) into gmGamePlayUI's ONE
|
||||
// m_doEndSession drain, so the grounded three-way branch applies
|
||||
// here too — the original OP3 port carried it only on the Options
|
||||
// button.
|
||||
ShowConfirmation(
|
||||
ResolveEndCharacterSessionConfirmMessage(),
|
||||
accepted =>
|
||||
{
|
||||
if (accepted)
|
||||
EndCharacterSessionWithRetailGates();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The shared confirmed-Yes drain — <c>gmGamePlayUI::UseTime
|
||||
/// @ 0x004EA3A0</c>'s exact three-way branch (see
|
||||
/// <see cref="RequestExitToCharacterSelection"/>'s doc for the
|
||||
/// pseudocode): grounded → <c>CPlayerSystem::LogOffCharacter(0)</c>;
|
||||
/// airborne → the mid-air refusal; no player → silent no-op.
|
||||
/// </summary>
|
||||
private void EndCharacterSessionWithRetailGates()
|
||||
{
|
||||
switch (_bindings.Options.IsGrounded())
|
||||
{
|
||||
if (accepted)
|
||||
case true:
|
||||
_bindings.Indicators.EndCharacterSession();
|
||||
});
|
||||
break;
|
||||
case false:
|
||||
_bindings.Options.DisplaySystemMessage(
|
||||
ClientTextRefusals.CantLogOffMidAir);
|
||||
break;
|
||||
case null:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign OP slice OP3: the Options panel's Exit to Character
|
||||
/// Selection button (element <c>0x10000203</c>) — D6's "behaves as Exit
|
||||
/// Game" adaptation (register row, same commit), PLUS retail's
|
||||
/// confirmation dialog and mid-air refusal, which DO port exactly.
|
||||
/// Selection button (element <c>0x10000203</c>), with retail's
|
||||
/// confirmation dialog and mid-air refusal. Logout round (2026-08-17):
|
||||
/// the D6 "behaves as Exit Game" adaptation (AD-76) is RETIRED — the
|
||||
/// grounded confirmed exit now runs the real in-world logoff flow
|
||||
/// (log-off animation, reverse wormhole, return to character select on
|
||||
/// the live connection).
|
||||
/// <c>gmGamePlayUI::UseTime @0x004EA3A0</c>'s drain, its EXACT three-way
|
||||
/// branch (review-fix round, 2026-08-11 — the original port collapsed
|
||||
/// this to a two-way `if/else` that fired the refusal outside player
|
||||
|
|
@ -2490,26 +2532,18 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
/// </summary>
|
||||
private void RequestExitToCharacterSelection()
|
||||
{
|
||||
ShowConfirmation(ResolveEndCharacterSessionConfirmMessage(), accepted =>
|
||||
{
|
||||
if (!accepted) return;
|
||||
|
||||
switch (_bindings.Options.IsGrounded())
|
||||
// Logout round (2026-08-17): the D6 "behaves as Exit Game"
|
||||
// adaptation (AD-76, retired with this change) is gone — the
|
||||
// confirmed, grounded exit now runs the REAL retail flow through
|
||||
// EndCharacterSessionWithRetailGates: log-off animation, reverse
|
||||
// wormhole, return to character select on the live connection.
|
||||
ShowConfirmation(
|
||||
ResolveEndCharacterSessionConfirmMessage(),
|
||||
accepted =>
|
||||
{
|
||||
case true:
|
||||
_bindings.Indicators.EndCharacterSession();
|
||||
break;
|
||||
case false:
|
||||
_bindings.Options.DisplaySystemMessage(ClientTextRefusals.CantLogOffMidAir);
|
||||
break;
|
||||
case null:
|
||||
// Retail's `else if (smartbox->player)` gate: outside
|
||||
// player mode (or with no live controller) there is no
|
||||
// player object for UseTime to test at all, so neither
|
||||
// the airborne refusal nor the logoff itself ever runs.
|
||||
break;
|
||||
}
|
||||
});
|
||||
if (accepted)
|
||||
EndCharacterSessionWithRetailGates();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2575,7 +2609,10 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
var callbacks = new Layout.OptionsPanelController.Callbacks(
|
||||
Toggle: () => ToggleWindow(WindowNames.Options),
|
||||
RequestExitToCharacterSelection: RequestExitToCharacterSelection,
|
||||
ExitGame: _bindings.Indicators.EndCharacterSession,
|
||||
// Logout round (2026-08-17): Exit Game keeps the app-exit
|
||||
// (window close → graceful-shutdown logoff); the in-world
|
||||
// return-to-charselect flow lives on EndCharacterSession.
|
||||
ExitGame: _bindings.Indicators.ExitGame,
|
||||
UseMouseTurningSettings: ApplyMouseTurningSettingsMacro,
|
||||
DisplaySystemMessage: _bindings.Options.DisplaySystemMessage,
|
||||
AfterApply: () => _bindings.Options.CommandBus().Publish(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue