8.8 KiB
Campaign CT — Character-panel retail parity (header identity, Titles page, resize/scrollbar, row alignment)
Status: PLANNED 2026-08-24 (owner gate report after the CA5/chat rounds). Execution model: Fable plans and coordinates; Sonnet implements each slice; Opus runs the dual-lens review (retail-faithful + architectural) per slice, then a fix round. No pushes to gitea until the owner says so. Register: this campaign retires AP-109 (inert Titles page) when CT3+CT4 land; every deviation a slice introduces adds its row in the same commit.
Owner report (2026-08-24, screenshots on file)
- Attribute/skill row icons misaligned vs retail.
- Retail keeps a margin between the value column and the border — the gutter reserved for the list scrollbar that appears when the window is resized shorter. We author no such margin and never show the scrollbar on Attributes.
- The character window is resizable in Y down to an authored minimum in retail; ours is not.
- Header identity block: retail shows the name, then " " (e.g. "Female Aluvian War Mage"), then "Non-Player Killer" / "Player Killer" / "Player Killer Lite" in pure white — on Attributes AND Skills. We show gender+heritage only, no PK line contract, color off.
- Level number color slightly off vs retail.
- Titles tab is inert (AP-109): retail lists all earned titles (sorted), shows the current display title, and lets the player set one ("Set as Display Title"); scrollbar with many titles; the header identity line updates live when the display title changes.
- All windows share retail's authored minimum-size behavior — resize clamps to the authored constraints everywhere.
Retail recon (verified 2026-08-24, this session)
Titles page — gmCharacterTitleUI
PostInit @0x0049A610binds: display-title text0x1000052F, "Set as Display Title" button0x10000535, title ListBox0x10000532. Registers notice handlers for the title-table / add-title / set-display-title notices.- Rows carry the title id in attribute
0x1000008E;AddTitleToList @0x0049A840resolves the display string viaCharacterTitleTable::GetCharacterTitleFromID(DAT title-string table — CT2 locates the DID) and inserts SORTED (FindSortedInsertPosition @0x0049A760). UpdateButtons @0x0049A500: the display button GHOSTS (state 0xd) when the selected row's title id ==mDisplayTitle; Normal (1) otherwise. Selection change (msg 4/0x43) re-runs it.- Clicking
0x10000535sendsCM_Social::Event_SetDisplayCharacterTitle(titleId)(ListenToElementMessage @0x0049A6D0). gmStatManagementUI::RecvNotice_SetDisplayCharacterTitle @0x004EFD50→ the stat panel refreshes its header when the display title changes.
Wire (ACE cross-checked)
- Inbound
CharacterTitleevent0x0029(already in ourGameEventType):u32 =1, u32 displayTitleId, u32 count, count × u32 titleId(GameEventCharacterTitle.cs). - Inbound
UpdateTitleevent0x002B:u32 titleId, u32 setAsDisplay(GameEventUpdateTitle.cs). - Outbound
TitleSetGameAction (GameActionSetTitle.cs):u32 titleId. Retail sender:CM_Social::Event_SetDisplayCharacterTitle.
Header identity — gmStatManagementUI::PostInit @0x004EFD90
Binds name 0x10000231, heritage line 0x10000232, PK line
0x10000233, level 0x1000023B, total XP 0x10000235, XP-to-level
0x10000238 + meter 0x10000236, luminance pair 0x100005C5/C6, list
box 0x1000023D. The refresh (vtable slot, near
UpdateExperience @0x004F0A70) composes the heritage line WITH the
display title; the PK strings are exactly "Player Killer" /
"Player Killer Lite" / "Non-Player Killer" (IsPK / IsPKLite —
cross-anchor CharExamineUI::SetAppraiseInfo @0x004B45F0). CT5 reads
the composing function verbatim before writing a line of C#.
CT4 contract (read verbatim 2026-08-24, Fable)
gmStatManagementUI::UpdateCharacterInfo @0x004F0770:
- Name line (
0x10000231):ACCWeenieObject::GetObjectName(player, NAME_SINGULAR)throughAllegianceData::GetFullName @0x005B6950(read verbatim): whenAllegianceSystem::GetTitle(rank, heritage, gender)yields an allegiance rank title, the line is "<sep @data_794098>"; otherwise the plain name. The owner's retail screenshot (plain "Dww") is the rankless case. - Heritage line (
0x10000232):AppraisalSystem::InqGenderHeritageDisplay(gender=Int 0x71, heritage=Int 0xBC, 0)("Female Aluvian"); then, whenCharacterTitleTable::GetCharacterTitleFromID(m_titleID)resolves,AppendText(separator @data_794358)+AppendText(titleString)→ "Female Aluvian War Mage". Recover the separator literal with the PE-read recipe (claude-memory/reference_pe_byte_decode.md). - Level (
0x1000023B):InqInt(0x19)present →Formatted(@data_7a0184); absent → literal@data_7b0f34. - PK line (
0x10000233):UpdatePKStatus @0x004F00A0— three-wayIsPK/IsPKLite/ neither → StringInfo from table enum0x10000001(= StringTable0x23000001, same compute_str_hash mechanism the chat labels use) with keysID_StatManagement_Header_PKStatus_PK/_PKL/_NPK. - Related key family for the footer/meter (already-shipped surfaces —
audit only if CT1 finds drift):
ID_StatManagement_Footer_*,ID_StatManagement_Header_XPToLevelMeterInfinity.
Already in-tree
- Tab/page ids wired (
TabTitlesId 0x10000538,TitlesPageId 0x10000539); pages currently show retail-authored closed visuals. - Header labels partially bound (
StatHeaderLine+PkStatusseams exist inCharacterStatController.Bind— content contract wrong). GameEventType.CharacterTitle/UpdateTitleenum entries exist; no parser, no state owner, no outbound builder.- The character window registers with
DatConstraintSource— authored min/max plumbing exists inRetailWindowFrame; Y-resize for this window and the list-scrollbar contract do not.
Slices
CT1 — DAT ground truth + pins. Live-DAT probe of layout
0x2100002E: attribute/skill row templates (icon x/y vs our hand-built
rows), the value-column right margin, header element fonts/colors
(level 0x1000023B color — item 5), Titles-page elements
(0x1000052F/32/35 geometry, row template, scrollbar), window
min/max constraints. Output: research doc + InstalledDat pins (the
tooltip/scrollbar-pin pattern). No production changes.
CT2 — Runtime title ownership + wire. Parse 0x0029/0x002B;
locate the DAT title-string table GetCharacterTitleFromID reads and
port the lookup; RuntimeCharacterState owns the title set + display
title (J4.3 owner; clears at generation reset); outbound TitleSet
builder behind a typed Runtime command; ordered change events for UI
and headless bots (#368 contract: hosts observe the same owner).
Conformance tests against ACE's writer shapes.
CT3 — Titles page UI. Bind the authored page through the standard
GUI classes (UiTemplateListBox/UiScrollbar/UiButton — zero
bespoke widgets): sorted rows via the ported title-table lookup,
selection, ghost-when-current logic (state 0xd contract), display-title
text, Set-as-Display round trip, scrollbar. Retires half of AP-109.
CT4 — Header identity block. Retail composition: name; " "; PK status line — authored fonts/colors (pure white per probe), live refresh on display-title change and PK status, identical on Attributes AND Skills pages. Level color from the authored element. Retires the rest of AP-109's UI half.
CT5 — Row alignment + value gutter. Reconcile our hand-built attribute/skill rows with the authored row templates from CT1: icon placement, name/value columns, the authored right margin that reserves the scrollbar gutter.
CT6 — Resize + scrollbar contract. Character window Y-resizable to
the authored minimum; the stat list shows its scrollbar when the
resized viewport overflows (the full-track/disabled behavior from the
2026-08-24 scrollbar work applies as-is); authored min/max constraint
enforcement verified as the STANDARD path for every registered window
(one shared mechanism in RetailWindowFrame/RetailWindowManager,
no per-window special cases).
CT7 — Connected gate. Test script
(docs/research/2026-08-24-campaign-ct-test-script.md), owner drive:
titles round trip against ACE (earn/set/display), header lines vs
retail side-by-side, resize behavior, row alignment screenshots.
Review protocol
Per slice: Sonnet implements → Opus dual-lens review (lens 1 retail-faithfulness vs the cited decomp anchors; lens 2 architecture — GUI-class standardization, Runtime ownership boundaries, no controller-side state) → fix round → full hermetic suite green. Commits to the worktree branch as slices land; no gitea push until the owner directs it.