docs: Campaign CT plan — character-panel parity (Titles page, header identity, resize/scrollbar, row alignment)

Owner-reported 2026-08-24 batch, recon-verified against gmCharacterTitleUI
@0x0049A610 / gmStatManagementUI::PostInit @0x004EFD90 / ACE's
CharacterTitle-UpdateTitle-TitleSet wire trio. Retires AP-109 when CT3/CT4
land. Fable plans, Sonnet implements, Opus dual-lens reviews; no push
until the owner directs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-24 21:05:19 +02:00
parent aa94ddebe3
commit 1cd4dfc92a

View file

@ -0,0 +1,139 @@
# 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)
1. Attribute/skill row **icons misaligned** vs retail.
2. 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.
3. The character window is **resizable in Y down to an authored
minimum** in retail; ours is not.
4. Header identity block: retail shows the name, then
**"<Gender> <Heritage> <DisplayTitle>"** (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.
5. **Level number color** slightly off vs retail.
6. **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.
7. **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 @0x0049A610` binds: display-title text `0x1000052F`,
"Set as Display Title" button `0x10000535`, title ListBox
`0x10000532`. Registers notice handlers for the title-table /
add-title / set-display-title notices.
- Rows carry the title id in attribute `0x1000008E`;
`AddTitleToList @0x0049A840` resolves the display string via
`CharacterTitleTable::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 `0x10000535` sends
`CM_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 `CharacterTitle` event `0x0029` (already in our
`GameEventType`): `u32 =1, u32 displayTitleId, u32 count,
count × u32 titleId` (`GameEventCharacterTitle.cs`).
- Inbound `UpdateTitle` event `0x002B`: `u32 titleId,
u32 setAsDisplay` (`GameEventUpdateTitle.cs`).
- Outbound `TitleSet` GameAction (`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#.
### Already in-tree
- Tab/page ids wired (`TabTitlesId 0x10000538`, `TitlesPageId
0x10000539`); pages currently show retail-authored closed visuals.
- Header labels partially bound (`StatHeaderLine` + `PkStatus` seams
exist in `CharacterStatController.Bind` — content contract wrong).
- `GameEventType.CharacterTitle/UpdateTitle` enum entries exist; no
parser, no state owner, no outbound builder.
- The character window registers with `DatConstraintSource` — authored
min/max plumbing exists in `RetailWindowFrame`; 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; "<Gender>
<Heritage> <DisplayTitle>"; 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**.