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

109 lines
5.3 KiB
Markdown

# 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.