acdream/docs/research/2026-08-14-la7b-character-selection-runtime.md

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 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:

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.