feat(launcher): LU9/LU10 — stop logs out for real, sessions read plainly, logout lands on character select
Five things from the user's gate. STOP NOW ACTUALLY LOGS OUT. The UI gave the client five seconds and then killed it. That is not enough for a graphical client to send its logout, wait for the server to acknowledge, and tear down a mapped 28 GB world — so Stop routinely ended in a kill, which sends the server nothing, which is exactly what leaves the account held. Thirty seconds now, with the kill still there as a genuine last resort, and the status says "Logging out…". THE SERVER-SIDE HOLD IS MODELLED INSTEAD OF DISCOVERED. A session that ends without the host running its own teardown may leave the account logged in server-side for minutes. Launching again inside that window does not queue or retry — it fails with a bare "CharacterList not received", which reads as a broken launcher rather than a busy server. The orchestrator now records whether each session ended gracefully (the host reported its own exit AND exited zero — a killed or crashed child can satisfy neither) and refuses that account for three minutes afterwards, saying how many seconds are left. A graceful exit never starts a hold. READABLE TERMINAL TEXT. "Exited: connection-error (code 5)" becomes "Could not reach the server — the server may hold this account for a few minutes"; "Exited: process-exit (code 0)" becomes "Exited gracefully — logged out cleanly". Live sessions still show the host's own status line, which is the most informative thing available while one is running. COLUMN HEADERS on the sessions list — ACCOUNT / CHARACTER / STATUS / DETAIL, sharing the row template's widths so they stay aligned. LOGOUT LANDS ON THE CHARACTER SCREEN (LU10). The toolbar X was already wired correctly: IndicatorBarController's EndCharacterSessionButtonId 0x100000FA runs retail's EndCharacterSession, and LiveSessionController's logout transaction already ends by resetting the world generation and calling CharacterSelectionState.Begin. What was missing is where that lands: the retained UI built its character-selection and character-creation bindings only when NO character selector was supplied, so a launcher-started session logged out into a client with no screen to return to. The selector decides how a session STARTS; it must not decide whether the select screen EXISTS. Both binding sets are now unconditional. The composition test that pinned the old gate is updated to pin the new contract — the retained UI must not branch on the selector at all — rather than being deleted. App 5380 passed, Launcher.Core 336, Launcher 76, Headless 169. Not pushed; the user is testing locally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
18bbd37779
commit
6ab5d8ce0f
7 changed files with 234 additions and 32 deletions
|
|
@ -1023,30 +1023,44 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
Keyboard: new KeyboardRuntimeBindings(
|
||||
d.InputDispatcher,
|
||||
d.KeyBindingsFilePath),
|
||||
CharacterSelection: d.Options.LiveCharacterSelector is null
|
||||
? new CharacterSelectionRuntimeBindings(
|
||||
() => late.GameRuntime.CharacterSelection,
|
||||
late.GameRuntime.CharacterSelectionHighlight,
|
||||
late.GameRuntime.CharacterSelectionEnter,
|
||||
late.GameRuntime.CharacterSelectionRequestDelete,
|
||||
late.GameRuntime.CharacterSelectionConfirmDelete,
|
||||
late.GameRuntime.CharacterSelectionRestore,
|
||||
late.GameRuntime.CharacterSelectionCancel,
|
||||
// Campaign LA gate round 2 finding 1: the SAME
|
||||
// window-close path GameplayInputCommandController's
|
||||
// Escape fallback uses (IGameplayWindowCommands.Close
|
||||
// /GameplayWindowCommands wrap this same d.Window.Close
|
||||
// delegate) — no separate exit path.
|
||||
d.Window.Close)
|
||||
: null,
|
||||
// LU10: built UNCONDITIONALLY, including when the launcher
|
||||
// supplied a character selector. The selector only decides how
|
||||
// this session STARTS — straight into the world instead of
|
||||
// pausing at the select screen. It must not decide whether the
|
||||
// select screen EXISTS, because the player can come back to it:
|
||||
// the toolbar's X (IndicatorBarController's
|
||||
// EndCharacterSessionButtonId 0x100000FA) runs retail's
|
||||
// EndCharacterSession, and LiveSessionController's logout
|
||||
// transaction ends by resetting the world generation and
|
||||
// calling CharacterSelectionState.Begin — i.e. it hands control
|
||||
// to exactly these bindings. Gating them on the selector meant a
|
||||
// launcher-started session logged out into a client with
|
||||
// nowhere to land.
|
||||
CharacterSelection: new CharacterSelectionRuntimeBindings(
|
||||
() => late.GameRuntime.CharacterSelection,
|
||||
late.GameRuntime.CharacterSelectionHighlight,
|
||||
late.GameRuntime.CharacterSelectionEnter,
|
||||
late.GameRuntime.CharacterSelectionRequestDelete,
|
||||
late.GameRuntime.CharacterSelectionConfirmDelete,
|
||||
late.GameRuntime.CharacterSelectionRestore,
|
||||
late.GameRuntime.CharacterSelectionCancel,
|
||||
// Campaign LA gate round 2 finding 1: the SAME
|
||||
// window-close path GameplayInputCommandController's
|
||||
// Escape fallback uses (IGameplayWindowCommands.Close
|
||||
// /GameplayWindowCommands wrap this same d.Window.Close
|
||||
// delegate) — no separate exit path.
|
||||
d.Window.Close),
|
||||
// Campaign CC slice CC4: same late-bound generation-capturing
|
||||
// seam as CharacterSelection above. RequestExit here is a
|
||||
// plain presentation action (closing the chargen screen and
|
||||
// letting character-management's own Tick keep re-drawing
|
||||
// itself underneath — see CharacterCreationUiController's
|
||||
// OnExit doc), NOT a Runtime command or a window-close.
|
||||
CharacterCreation: d.Options.LiveCharacterSelector is null
|
||||
? new CharacterCreationRuntimeBindings(
|
||||
// LU10: unconditional for the same reason as CharacterSelection
|
||||
// above — a player who logs out back to the select screen can
|
||||
// create a character from there, so the screen behind it must
|
||||
// exist regardless of how this session started.
|
||||
CharacterCreation: new CharacterCreationRuntimeBindings(
|
||||
() => late.GameRuntime.CharacterCreation,
|
||||
late.GameRuntime.CharacterCreationSelectHeritage,
|
||||
late.GameRuntime.CharacterCreationSelectGender,
|
||||
|
|
@ -1076,8 +1090,7 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
RandomizeAppearance: late.GameRuntime.CharacterCreationRandomizeAppearance,
|
||||
RandomizeClothing: late.GameRuntime.CharacterCreationRandomizeClothing,
|
||||
GetSkillScore: chargenSkillScoreResolver.Resolve,
|
||||
OpenOnStart: d.Options.OpenCharacterCreationOnStart)
|
||||
: null);
|
||||
OpenOnStart: d.Options.OpenCharacterCreationOnStart));
|
||||
RetailUiRuntime runtime = lease.Mount(
|
||||
() => RetailUiRuntime.CreateUninitialized(bindings));
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.UiRuntimeMounted);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue