feat(runtime): own character selection flow
This commit is contained in:
parent
6c4cd2bbc6
commit
0e82cbf700
20 changed files with 2396 additions and 33 deletions
109
docs/research/2026-08-14-la7b-character-selection-runtime.md
Normal file
109
docs/research/2026-08-14-la7b-character-selection-runtime.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue