5.3 KiB
LA7b character-selection Runtime evidence
Date: 2026-08-14
This note records the retail evidence and the presentation-independent state and flow implemented by Campaign LA slice LA7b. The retained character screen is deliberately deferred to LA8.
Named-retail evidence
The implementation was derived from
docs/research/named-retail/acclient_2013_pseudo_c.txt before code was
written. The controlling functions are:
gmCharacterManagementUI::ResetPreviouslySelectedCharacterSlot(0x004ebff0): clears the persisted selected avatar, selected guid, list index, and character-generation slot.gmCharacterManagementUI::SelectCharacter(0x004ec160): resolves the clicked guid back to theCharacterSetslot and persists that exact slot.gmCharacterManagementUI::UpdateButtons(0x004ec240): a missing or greyed selection disables Enter and Delete; a selected greyed character hides Delete and shows Restore.gmCharacterManagementUI::RebuildCharacterList(0x004ec3a0): emits every activeCharacterSetentry, records the previous/slot selection, chooses the first non-greyed entry as the ordinary fallback (the first row remains the all-grey fallback), sorts names withwcscmp, and then moves greyed rows to the tail without losing their identity.gmCharacterManagementUI::MakeDeleteCharacterConfirmationDialog(0x004ecca0) andCloseDeleteCharacterDialog(0x004ed4a0): deletion is modal, and confirmation sends the persisted selected avatar.gmCharacterManagementUI::ListenToElementMessage(0x004ed5a0): Restore sends the selected guid, Enter is accepted from the button and list-row activation, and both delete/restore open retail's wait dialog.gmCharacterManagementUI::EnterGame(0x004ed440): entry is allowed only for a non-zero selected guid whoseGetGreyedOutFor(GetSlot(guid))value is zero.CharacterSet::GetSlot(0x004fdf60) andGetGreyedOutFor(0x004fdfa0) prove that delete/enter retain the original wire slot even after display sorting, and that any non-zero grey value is disabled.CPlayerSystem::Handle_CharacterError(0x0055d5d0) clears the ready and awaiting-logon latches before forwarding the typed error. The downstreamgmUIFlow::RecvNotice_CharacterError(0x0047a7c0) bounds its real display switch to enum values belowCHAR_ERROR_NUM_ERRORS; the0x19member is a count sentinel, not a message.
Equivalent state pseudocode:
on roster:
remember selected guid
rows = every active CharacterSet entry, retaining original slot
fallback = first row, replaced by first non-greyed row in wire order
sort rows by ordinal/wcscmp name
stable-partition non-greyed before greyed
selected = previous guid if still present, otherwise fallback
buttons(selected):
none or greyed -> Enter disabled, Delete disabled
greyed -> Delete hidden, Restore visible/enabled
active -> Delete visible/enabled, Restore hidden
enter(selected):
require guid != 0 and grey == 0
use retained wire slot through the existing EnterWorld transaction
The CharacterList.DeletedCharacters array is not projected into this list:
retail rebuilds from the active CharacterSet; pending-delete identities are
the greyed entries in that active set. No entry is filtered merely because it
is greyed.
Runtime ownership and flow
GameRuntime owns one LiveSessionController, which owns one
RuntimeCharacterSelectionState for the exact WorldSession generation.
The owner contains the canonical roster, wire slots, highlight, delete
confirmation, delete/restore progress, mapped error, revision, lifecycle, and
generation. App receives only the borrowed IRuntimeCharacterSelectionView;
all mutation uses synchronous generation-gated typed commands. Ordered deltas
use one monotonic sequence and preserve re-entrant publication order while
isolating observer failures.
A graphical launch with no explicit selector sets
AwaitCharacterSelection. Connect reports and adopts the roster, starts the
sole pre-world receive loop, and returns AwaitingCharacterSelection without
calling EnterWorld. A typed Enter command continues the same session and
generation through the established two-phase EnterWorld path. Explicit
graphical selectors and direct/headless callers retain the established
first-available fallback because the new flag defaults to false.
Immediate/headless entry keeps the original blocking receive/sweep pump until ServerReady. Only a host that actually pauses at character selection starts the asynchronous pre-world receiver. This preserves reliable-transport resend/NAK timing and avoids two concurrent socket readers.
LA7a's wire contracts are routed as follows:
- delete
0xF655: account plus retained active slot, LoginQueue; - restore
0xF7D9: selected guid, ControlQueue (AD-97 remains the recorded guid-only adaptation); - delete ack, restore
0xF643, refreshedCharacterList, andCharacterError 0xF659: ordered UIQueue input.
ACE can silently return from restore for an unknown guid. Restore is therefore
a fire-and-observe command: it installs no command gate and never waits for a
reply. A later matching response updates the same owner; a fresh roster,
entry attempt, reset, reconnect, or disposal disarms stale response
correlation. CharacterError.NumErrors is ignored without a revision or
delta and can never become presentation text.