# 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 the `CharacterSet` slot 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 active `CharacterSet` entry, 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 with `wcscmp`, and then moves greyed rows to the tail without losing their identity. - `gmCharacterManagementUI::MakeDeleteCharacterConfirmationDialog` (`0x004ecca0`) and `CloseDeleteCharacterDialog` (`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 whose `GetGreyedOutFor(GetSlot(guid))` value is zero. - `CharacterSet::GetSlot` (`0x004fdf60`) and `GetGreyedOutFor` (`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 downstream `gmUIFlow::RecvNotice_CharacterError` (`0x0047a7c0`) bounds its real display switch to enum values below `CHAR_ERROR_NUM_ERRORS`; the `0x19` member is a count sentinel, not a message. Equivalent state pseudocode: ```text 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`, refreshed `CharacterList`, and `CharacterError 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.