acdream/docs/research/2026-08-14-la8-character-management-ui.md

120 lines
7 KiB
Markdown

# LA8 retained character-management UI evidence
Date: 2026-08-14
This note records the retail and installed-DAT evidence for Campaign LA slice
LA8, plus the exact ownership and presentation boundary implemented by the
slice. LA7b remains the authority for pre-world Runtime and wire behavior.
## Named-retail evidence
The implementation was derived from
`docs/research/named-retail/acclient_2013_pseudo_c.txt` and the corresponding
`acclient.h` definition before the screen was written.
- `DBObj::GetDIDByEnum` (`0x004153A0`) forwards to
`DBCache::GetDIDFromEnumStatic`; retail resolves the category/table mapping
before loading a LayoutDesc.
- `gmCharacterManagementUI::gmCharacterManagementUI` (`0x004EC8F0`) calls
`UIMainFramework::CreateAndAddRootElement(0x10000005, 0x1000039A)`, then
binds ListBox `0x1000039D`, Create `0x100003A0`, Enter `0x100003A2`, Delete
`0x1000039F`, and Restore `0x1000039E`.
- The verbatim header at `acclient.h:56545` declares exactly that ListBox,
those four button pointers, the selected row/guid, and four dialog contexts.
It declares no viewport, `gmCG3DView`, or preview owner.
- `RebuildCharacterList` (`0x004EC3A0`) creates each row through
`AddItemFromTemplateList`, then resizes it using signed integer division:
`max(listHeight / max(rosterCount, allowedSlots), listHeight / 10)`. Thus a
320-pixel list with five allowed slots uses 64-pixel rows, while rosters over
ten clamp at 32 pixels. It retains character identity, displays pending
deletion in red, sorts by ordinal name, moves greyed entries to the tail,
and restores/falls back selection. LA8 preserves the already canonical LA7b
display order and identity instead of sorting an App copy.
- `SelectCharacter` (`0x004EC160`) and `UpdateButtons` (`0x004EC240`) establish
the highlight and button matrix: no or greyed selection disables Enter and
Delete; an active selection shows/enables Delete; a greyed selection hides
Delete and shows/enables Restore.
- `ListenToElementMessage` (`0x004ED5A0`) routes the list selection message,
button clicks, and row-template `0x100003A5` activation message `0x1A`.
Double-activating a row calls `EnterGame` (`0x004ED440`).
- `MakeDeleteCharacterConfirmationDialog` (`0x004ECCA0`) uses retail dialog
type 5 and compares the typed response with the localized DELETE response
case-insensitively. `MakePleaseWaitDialog` (`0x004ECED0`) and
`MakeEnteringWorldDialog` (`0x004ED090`) use the wait machinery. Error
presentation enters through `MakeErrorMessageDialog` (`0x004ECB10`). The
destructor (`0x004EC080`) closes every owned dialog context.
The shared dialog factory switch supplies catalog roots/classes used here:
message type 3 is root `0x24` / class `0x17`; confirmation-text-input type 5
is root `0x2C` / class `0x15`; the existing wait type 2 is root `0x31` /
class `0x19`. The message button is `0x26`. Type 5 uses field `0x2C`, accept
`0x2E`, reject `0x2F`, and result property `0x9C`.
## Installed-DAT proof
The permanent read-only acceptance probe is
`tests/AcDream.App.Tests/UI/Layout/CharacterManagementLiveDatTests.cs`. Run it
with `ACDREAM_PROBE_LIVE_MOUNT=1`; it reads the ordinary
`%USERPROFILE%/Documents/Asheron's Call` DAT set unless `ACDREAM_DAT_DIR`
overrides the location. It uses production `DatCollection`,
`RetailDataIdResolver`, and `LayoutImporter`; it does not write the DATs.
When the opt-in flag or installed data is absent, discovery records an explicit
skip rather than adding a no-op pass to default suite totals.
The installed September-2013 data proves:
- enum category/table 5 maps `0x10000005` to concrete LayoutDesc DID
**`0x21000004`**;
- selected root `0x1000039A` is 800 x 600 with eight authored children;
- the root itself authors image media `0x06007576`; that proves a retained
layout asset, not a separate render-loop background scene;
- its ListBox template is `{ 0x21000004, 0x100003A5 }`;
- the template is a 160 x 16 `UiButton`, font `0x40000009`, with Normal,
NormalRollover, NormalPressed, Highlight, HighlightRollover, and the authored
`0xFFFFFFFF` default state;
- the authored captions are Create Character, ENTER, DELETE, and RESTORE;
- neither the selected root nor any descendant is a `UiViewport`;
- enum-table-5 dialog key 2 maps to catalog DID `0x2100003C`, containing the
type-3 and type-5 roots/children above;
- string table `0x23000002` contains DELETE, Please Wait, Entering World, and
the delete-confirmation template. The template has the PLAYER variable and
resolves it into the selected character name.
## Ownership, composition, and lifecycle
`RetailUiRuntime` imports the exact enum-resolved root only for a graphical
launch with no explicit character selector. Its focused binding borrows
`IRuntimeCharacterSelectionView`; every highlight, enter, delete-request,
delete-confirm, restore, and cancel action crosses the existing deferred
adapter as a generation-capturing Runtime command. App retains no gameplay
mirror. Explicit-selector graphical launches keep their existing flow, and
headless does not compose this App presentation.
The controller instantiates the authored row template in Runtime display
order, projects red pending-delete rows and the exact button matrix, and opens
the shared retail dialogs. Delete wait survives the opcode-only acknowledgement
until the fresh roster arrives. Restore is fire-and-observe: a silent ACE
no-reply ends only when Runtime expires its correlation; retail's Please Wait
opens before the synchronous restore command and closes immediately if that
command rejects or throws. Entering-world wait opens before the existing
synchronous Enter command; error, reset, reconnect, missing/displaced adapter,
and disposal close owned contexts without re-entrant commands. A failed
transient row-template import leaves the Runtime revision unconsumed and
retries on the next frame. Initial dialog-catalog, character root, and string
misses likewise retry on later ticks without mounting a duplicate root or
controller. Dialog presenter/catalog failures move their contexts to an
internal retry ledger, so UI callbacks do not retain poisoned active/queued
entries and the same context can appear after resource recovery. Priority
contexts remain ahead of ordinary retries and preserve retail's nested
preemption order when creation recovers. The mount coordinator owns a detached
controller before attaching its root or running the first template-resolving
tick; any partial failure disposes that exact controller before retry, so roots
and handlers cannot accumulate.
There is deliberately no 3D preview and no claimed character-select background
scene. The screen root remains neutral with respect to render-loop background
composition. LA11's user visual gate owns that unresolved visual choice, plus
the live local-ACE delete/restore check. Because Enter currently completes its
established ServerReady transaction synchronously, LA11 must also verify that
the entering-world wait is perceptible on the real frame path; this slice does
not introduce a second queue or lifecycle owner merely to force a paint.