# Campaign CT slice CT1 — DAT ground truth for the character panel (0x2100002E) **Status:** RESEARCH COMPLETE 2026-08-24. No production code changed in this slice. Findings feed CT2–CT6 (`docs/plans/2026-08-24-character-panel-parity-campaign.md`). Method: temporary `Assert.Fail` probe tests (deleted before commit; the pattern is preserved in `tests/AcDream.App.Tests/UI/Layout/ScrollbarSkinLiveDatTests.cs` and its sibling `CharacterPanelLiveDatTests.cs` written by this slice) against the installed DAT set (`%USERPROFILE%\Documents\Asheron's Call`), driven through `LayoutImporter.ImportInfos` / `ElementInfo` (`src/AcDream.App/UI/Layout/ElementReader.cs`, `src/AcDream.App/UI/Layout/LayoutImporter.cs`). ## 1. Header elements (gmStatManagementUI content, sub-layout under 0x2100002E) All header elements are DUPLICATED — the imported 0x2100002E tree carries two structurally identical copies of the whole header block, one reached through the Attributes page chain (`0x10000227 > 0x1000022B > 0x10000226 > 0x10000230`) and one through the Skills page chain (`... > 0x1000022C > ...`). **The two copies are geometrically and stylistically IDENTICAL** — this is the "duplicated stat-management branches" quirk documented on `PrepareSkillScrollbar` in `CharacterStatController.cs`. **Caveat: this "identical" claim rests on the deleted probe tests, not the committed pins.** `CharacterPanelLiveDatTests.HeaderElements_AuthorExpectedFontsAndColors` only asserts font/color/margin equality across the two copies (and the 2-count) — it does not assert X/Y/W/H geometric equality between them. Treat "geometrically identical" as probe-session observation, not a pinned fact, until a future slice adds a geometry-equality assertion. **Coordinate frame:** every `X,Y` below is **parent-relative**, not page-relative — e.g. Name `0,0` means the top-left of its immediate parent container (`0x10000230`), not the top-left of the Attributes/ Skills page. Cross-reference `docs/research/2026-06-25-character-window-faithful-spec.md` for the page-relative numbers if you need the header block's position within the page itself; do not mix the two frames when placing elements. | Element | Id | X,Y | W×H | HJustify | VJustify | FontDid | FontColor | Outline | Margins | |---|---|---|---|---|---|---|---|---|---| | Name | 0x10000231 | 0,0 | 230×20 | Center | Center | 0x40000001 | white (1,1,1,1) | false | 0 | | Heritage line | 0x10000232 | 0,20 | 230×15 | Center | Center | 0x40000002 | white (1,1,1,1) | false | L5 R5 | | PK line | 0x10000233 | 0,35 | 230×15 | Center | Center | 0x40000002 | **white (1,1,1,1)** | false | 0 | | Level | 0x1000023B | 235,35 | 65×50 | Center | Center | 0x40000010 | **(1, 0.9490196, 0.49803922, 1)** | **true** | 0 | | Total XP | 0x10000235 | 130,70 | 100×18 | Right | Center | 0x40000000 | white | false | 0 | | XP meter | 0x10000236 (Type 7) | 0,88 | 230×17 | — | — | — | — | — | — | | XP-next label (child of meter) | 0x10000237 | 0,0 | 130×17 | Left | Center | 0x40000000 | white | false | 0 | | XP-to-level value (child of meter) | 0x10000238 | 130,0 | 100×17 | Right | Center | 0x40000000 | white | false | 0 | | Luminance label | 0x100005C5 | 0,52 | 110×18 | Left | Center | 0x40000000 | white | false | 0 | | Luminance value | 0x100005C6 | 110,52 | 120×18 | Right | Center | 0x40000000 | white | false | 0 | Header block container (0x10000230) is 300×110 inside the shared prototype layout 0x21000045 — the character content column itself is only 230px wide (name/heritage/PK/XP all live in that 230px column), with a 5px vertical divider (0x10000239, X=230, W=5) separating it from the level box (X=235..300, matching the plan's "Level area (65,50)" spec). **Confirmations vs. the owner report / plan:** - Item 4 (PK line pure white): CONFIRMED — `0x10000233` FontColor is exactly white, not the "color off" state the owner reported. The bug is purely in `CharacterStatController.Bind`'s runtime color choice (`Body` = parchment `(0.92,0.90,0.82,1)`), not a DAT-reading gap. CT4 must switch the PK-line color to `Vector4.One`. - Item 5 (level color): CONFIRMED DIVERGENT. DAT-authored level color is `(1, 0.9490196, 0.49803922, 1)` (a pale gold, ~RGB 255/242/127) **with Outline=true**. `CharacterStatController.Gold` is currently `(1, 0.82, 0.36, 1)` (a deeper orange-gold) with no outline applied. CT4 should read the DAT FontColor + Outline directly instead of hand-picking a runtime color, matching how `LevelId`'s dat FontDid is already honored. ## 2. Stat list (0x1000023D) + row templates `0x1000023D` (Type 5 ListBox) is duplicated the same harmless way as the header (once per Attributes/Skills page chain, identical geometry both times): `X=0 Y=112 W=300 H=160`, `ScrollbarElementId=0x1000023E` (scrollbar at `X=281 W=16 H=160`, i.e. always reserved, whether or not it's shown). Its authored `TemplateList` (dat property 0x64) names FIVE same-layout-family entries, all in **LayoutDesc 0x21000045**: ``` 0x10000248, 0x10000249, 0x1000024A, 0x1000024B, 0x1000024C ``` These are NOT reachable by walking `ImportInfos(dats, 0x21000045u)`'s built tree — `LayoutImporter.ImportInfos` intentionally filters out same-layout template-list targets (the `#375` fix documented in `LayoutImporter.ImportInfos`'s own comment: "retail never instantiates a template-list element as a live widget… building them here parked two live prototype rows… over and outside the framed panel"). The correct read path — and the one CT5 must use — is the **targeted single-root overload**: `LayoutImporter.ImportInfos(dats, 0x21000045u, templateElementId)`, the same seam `UiTemplateListBox`'s `TemplateResolver` already uses for other authored row templates. Dumping all five with that overload: ### 0x10000248 — the ONE shared data-row template (icon + name + value) ``` Id=0x10000248 Type=3 (container) X=0 Y=0 W=282 H=20 StateMedia[Normal] File=0x06004CC2 DrawMode=1 StateMedia[Highlight] File=0x06000F93 DrawMode=1 Id=0x10000129 Type=3 (icon slot) X=0 Y=0 W=20 H=20 (no own media — set per-row at runtime) Id=0x1000012A Type=0xC (name text) X=25 Y=0 W=150 H=20 HJustify=Left FontDid=0x40000001 white Id=0x1000012B Type=0xC (value text)X=175 Y=0 W=100 H=20 HJustify=Right FontDid=0x40000001 white ``` This is the single row template used for BOTH the attribute rows AND the skill rows (retail's `gmAttributeUI`/`gmSkillUI` share it). Ground truth for item 1 (icon alignment) and item 2 (value-column gutter): - **Icon: 20×20, flush at the row's left edge (X=0), full row height.** Current code (`CharacterStatController.IconSize = 16f`, `RowPadX = 4f`) draws a 16×16 icon at X=4 — smaller AND offset from retail's flush-left 20×20. This is the "icons misaligned" bug (item 1). - **Name column: X=25, W=150 (fixed pixel widths, not a width fraction).** Current code computes `nameX = RowPadX + IconSize + IconGap` (4+16+6=26, off by one from retail's 25) and `nameW = width * 0.60` (a content-relative fraction retail does not use at all — retail's name column is a FIXED 150px regardless of the 282px row width). - **Value column: X=175, W=100, right-justified.** Value's right edge sits at X=275. **The row template itself is 282px wide, so there is a 7px gap between the value's right edge and the row's own right edge** — this is the "authored margin between the value column and the border" the owner reported (item 2), confirmed as exactly 7px at the row-template level. - **The bare authored rectangles** (no derived arithmetic — see the caveat below): ListBox `0x1000023D` is `X=0 Y=112 W=300 H=160`; its scrollbar `0x1000023E` is `X=281 W=16 H=160`; the data row template is `W=282`; the value column inside the row is `X=175 W=100`, right-justified, 7px from the row's own right edge (`282 - (175+100) = 7`). - **These numbers do NOT compose into a tidy "gutter" story — do not infer one.** `300 - 281 = 19`, not the row's 282px width's complement (`300 - 282 = 18`); and the 282px row actually OVERLAPS the 281px scrollbar band by 1px (`281 < 282`). An earlier draft of this doc described the row as "inset by 18px to clear an always-reserved scrollbar gutter" and summed 18+7=25 — that decomposition does not close against the authored numbers above and is **inference, not fact**; withdrawn. CT5 must implement the authored numbers directly (row width 282 — matches the existing `SkillContentWidth = 282f` constant already in the code; value column X=175 W=100; scrollbar X=281 W=16), never a derived `listWidth - 18` or similar formula. - Row background: `Normal` state file `0x06004CC2`. **The description "generic panel-chrome fill used elsewhere client-wide" is UNVERIFIED** — no cross-reference sweep for other consumers of `0x06004CC2` was run this slice; only the file id itself, as authored on this specific template, is pinned. `Highlight` state file **`0x06000F93`**. ### SEALED VERDICT: RowHighlightSprite is wrong, not merely flagged Retail's selected attribute/skill row draws the row template's Highlight state media — `0x06000F93`, not `CharacterStatController`'s current `0x06001397u`. This is no longer a hedge; the decomp confirms the mechanism end to end: - `gmAttributeUI::UpdateSelection @0x0049DEE0` calls `SetState(selected ? 6 : 1)` on the row. - `InfoRegion::SetState @0x004F0EE0` forwards that state to the row element instantiated from template `0x10000248` — the exact template this slice dumped, whose Highlight-state file is `0x06000F93`. - State `6` IS `UIStateId.Highlight` — so the selected row draws `0x10000248`'s own `Highlight` media, not a separately-chosen sprite. `CharacterStatController.cs`'s comment near lines 111–113 ("matches retail... sprite 0x06001397 visual intent") is **falsified for this element**. CT5 must correct `RowHighlightSprite` to `0x06000F93` for the STAT rows and should reconsider `UseSelectionBars`/`HighlightBg`, which currently emulate the wrong sprite's art (a translucent gold tint tuned to look like `0x06001397`'s dark bars, not `0x06000F93`'s actual look). **`0x06001397` is not a phantom constant — it is legitimate ELSEWHERE.** The spellbook row prototype `0x10000343` has a separate selected-overlay CHILD element `0x10000342` whose media IS `0x06001397` (`UIElement_UIItem::SetSelectedState @0x004E1240` mechanism — a different code path from `InfoRegion::SetState`, and a different visual composition: an overlay child, not a state-swap on the row itself). CT5 must correct the STAT rows ONLY and must NOT touch `SpellbookRowStyle.cs` or its tests — `0x06001397` is correct there. ### 0x10000249 / 0x1000024A / 0x1000024B / 0x1000024C — skill SECTION HEADER captions ``` Id=0x10000249 Type=0xC X=0 Y=0 W=280 H=20 HJustify=Left Margins L5 R5 FontDid=0x40000001 StateMedia[]=0x06000F90 Id=0x1000024A Type=0xC X=0 Y=0 W=280 H=20 HJustify=Left Margins L5 R5 FontDid=0x40000001 StateMedia[]=0x06000F86 Id=0x1000024B Type=0xC X=0 Y=0 W=280 H=20 HJustify=Left Margins L5 R5 FontDid=0x40000001 StateMedia[]=0x06000F98 Id=0x1000024C Type=0xC X=0 Y=0 W=280 H=20 HJustify=Left Margins L5 R5 FontDid=0x40000001 StateMedia[]=0x06000F89 ``` These are single full-width caption bars (no icon/name/value split), 280px wide (2px narrower than the data row — no scrollbar-gutter inset needed since they never scroll independently). Their sprites are an EXACT match for the existing constants already in `CharacterStatController.cs`: | Constant | Value | Probe file | Match | |---|---|---|---| | `SkillHeaderSpecializedSprite` | 0x06000F90 | 0x10000249 | YES | | `SkillHeaderTrainedSprite` | 0x06000F86 | 0x1000024A | YES | | `SkillHeaderUntrainedSprite` | 0x06000F98 | 0x1000024B | YES | | `SkillHeaderUnusableSprite` | 0x06000F89 | 0x1000024C | YES | These four are correctly ported already; no CT5 work needed here. ### Row-height divergence (CT5 gold, found this fix round) The row template above authors `H=20` (line `Id=0x10000248 Type=3 (container) X=0 Y=0 W=282 H=20`, already pinned by `CharacterPanelLiveDatTests.AttributeRowTemplate_...`). Current code matches this for skill rows (`CharacterStatController.SkillRowHeight = 20f`) but NOT for attribute rows, which use a separate `CharacterStatController.RowHeight = 22f` constant. CT5 must fix the attribute-row path to 20px; there is no authored basis for 22 anywhere in the row template. ### Row instantiation + icon-DID anchors (CT5 gold, found this fix round) `InfoRegion::InfoRegion @0x004F1450` instantiates each stat row via `AddItemFromTemplateList(listBox, 0, ...)` — template index **0**, i.e. `0x10000248`, the shared data row confirmed above — and binds `0x1000012A` (label), `0x1000012B` (value), `0x10000129` (icon) via `UIRegion::SetImageByDID(icon, did, 3)` (icon draw mode 3). Per-attribute icon DIDs come from `DBObj::GetDIDByEnum(statEnum, category 0x10000002)` in `gmAttributeUI::PostInit @0x0049DB70` — a THIRD consumer of the `GetDIDByEnum` master-map mechanism documented in §5 below (alongside the title EnumMapper/StringTable pair and `RetailKeyNames`), which is enough precedent that CT2/CT5 should factor a shared `GetDIDByEnum(enumValue, category)` helper instead of hardcoding a third independent DID pair. `gmSkillUI::RebuildSkillList @0x0049C3A0` adds template indices **1–4** (`0x10000249..0x1000024C`) for the section-header captions, confirming the section-header order already pinned by `SkillSectionHeaderTemplates_MatchExistingSpriteConstants` above: Specialized, Trained, Untrained, Unusable. ## 3. Titles page (0x10000539 subtree, imported as part of 0x2100002E) `0x10000539` (Type 0x10000046, the Titles page container) is NOT duplicated like the Attributes/Skills content — it appears once, as a direct child of the tab-control root `0x10000227` (siblings with the Attributes/Skills tab buttons and the Titles tab button `0x10000538`). Geometry: `X=0 Y=25 W=300 H=575` (fills the window below the 25px tab bar). | Element | Id | X,Y | W×H | Notes | |---|---|---|---|---| | (unlabeled caption) | 0x1000052E | 8,20 | 270×18 | Left, margin L6, FontDid 0x40000001 white — likely "Current Title:" caption | | **Current display title text** | **0x1000052F** | 8,40 | 270×18 | Center, margins L5 R5, FontDid 0x40000001 white | | divider | 0x10000530 | 0,60 | 300×9 | sprite 0x06001420 | | (unlabeled caption) | 0x10000531 | 8,70 | 270×18 | Left, margin L6 — likely "Titles Earned:" caption | | **Title ListBox** | **0x10000532** | 8,90 | 270×455 | ScrollbarElementId=0x10000533; TemplateList: LayoutDid=0x2100005E, ElementId=0x10000536 | | Title scrollbar | 0x10000533 | 280,90 | 16×455 | shared `RetailScrollbarChrome` media (thumb/up/down ids identical to the base skin pinned by `ScrollbarSkinLiveDatTests`) | | divider | 0x10000534 | 0,550 | 300×9 | sprite 0x06001420 | | **"Set as Display Title" button** | **0x10000535** | 53,560 | 200×32 | MinWidth=65, margins L7 R7, FontDid 0x40000001 white, DefaultState=**Ghosted** (matches the plan's `UpdateButtons` ghost-when-current contract); three-slice chrome children 0x100002CE/CF/D0 with Normal/Normal_rollover/Normal_pressed/**Ghosted** states each | ### Title row template — LayoutDesc 0x2100005E, element 0x10000536 ``` Id=0x10000536 Type=3 (container) X=0 Y=0 W=270 H=24 StateMedia[DirectState, key ""] File=0x06004CCA DrawMode=3 StateMedia[Highlight] File=0x06001AAF DrawMode=1 Id=0x10000537 Type=0xC (text) X=0 Y=0 W=270 H=24 HJustify=Left Margins L6 R6 FontDid=0x40000001 white ``` A single-line text row, no icon column (titles have no per-row icon in retail) — 24px tall vs. the stat rows' 20px. Row width 270 matches the ListBox content width exactly (`0x10000532` is 270 wide, with its scrollbar `0x10000533` living OUTSIDE that width at X=280 — unlike the stat list, the title row template does NOT need its own internal scrollbar-gutter inset because the ListBox width itself already excludes the scrollbar column). Same targeted-root import gotcha as the stat templates applies here: `LayoutImporter.ImportInfos(dats, 0x2100005Eu)` (the whole-layout overload) returns the SAME element `0x1000052D`, but does NOT surface `0x10000536` as a reachable child. **`0x1000052D` is not a throwaway container** — per the `BaseElement`/`BaseLayoutId` table in `docs/research/2026-06-25-character-window-faithful-spec.md` (line ~19), `0x1000052D` is the authored `BaseElement` that the Titles page root `0x10000539` inherits its content from (`0x10000539`'s `BaseLayoutId` is `0x2100005E`, `BaseElement` is `0x1000052D`) — it is the real authored Titles-page content, just reached by a different path than the live-mounted tree. Use `LayoutImporter.ImportInfos(dats, 0x2100005Eu, 0x10000536u)` (the targeted single-root overload) to reach the row template `0x10000536` underneath it. ## 4. Window min/max constraints ### Character window (0x2100002E) root `LayoutImporter.ImportInfos(dats, 0x2100002Eu)` returns element `0x10000227` (Type 0x8, TabControl) as the tree root — this IS the top-level element retail's `RetailUiRuntime.MountCharacter()` mounts via `RetailWindowFrame.Mount(..., layout.Root, ...)`. **It authors NO MinWidth/MinHeight/MaxWidth/MaxHeight properties** (dat properties 0x3F/0x3E/0x3D/0x3C all absent — probe shows every one of `MinW/MinH/MaxW/MaxH` blank for 0x10000227 and every element under it, including the footer, header, and Titles page). ### Chat window (0x2100006F) root, for comparison `LayoutImporter.ImportInfos(dats, 0x2100006Fu)` returns element `0x10000600` (Type 0x10000050, a self-contained "window" element that directly includes its own dragbar (Type 2), border frame (Type 3), and FOUR resize-grip corners (Type 9) as children — none of which the character layout's root has). It DOES author constraints: **MinWidth=300, MinHeight=100, MaxWidth=2000, MaxHeight=2000.** ### Correction to the plan The plan's "Already in-tree" section states: *"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."* Read literally this implies the character window's `RetailWindowFrame.Mount` call already sets `DatConstraintSource`. **It does not.** `RetailUiRuntime.MountCharacter()` (`src/AcDream.App/UI/RetailUiRuntime.cs`, ~line 4043) constructs `RetailWindowFrame.Options` with `ResizeY = true`, `ResizableEdges = ResizeEdges.Bottom`, `ConstrainResizeToParent = true` — but **no `DatConstraintSource`, `MinHeight`, or `MaxHeight` field at all**, unlike e.g. `MountSideVitals()` (~line 1474) which explicitly sets `DatConstraintSource = info` from its own imported root. This is consistent with what CT1 also found in the DAT itself: 0x2100002E's root authors no size constraints to plumb through in the first place — chat's window-frame elements are its own self-contained LayoutDesc, while 0x2100002E is CONTENT ONLY (tab bar + pages), with retail's window chrome supplied by a separate mechanism. Treat the "Already in-tree" plan bullet as **inaccurate**: start CT6 from the decomp for the window-frame class instead of assuming the wiring is already 90% done. The paragraph below replaces this doc's earlier "likely a hardcoded ResizeTo/SetMinSize call" guess with the verified mechanism. ### Verified resize mechanism (2026-08-24, supersedes the hypothesis above) `UIElement::ResizeTo @0x00463C30` clamps ONLY via element attributes — `0x3C` (clamp-max-height), `0x3E` (clamp-min-height), `0x3D` (clamp-max-width), `0x3F` (clamp-min-width) — read off `this`, the element actually being resized. A decomp-wide grep for writers of those four attributes turns up NOTHING: no runtime code ever sets them at runtime. The clamp source is exclusively **authored DAT properties on whichever element `ResizeTo` is called against, full stop.** There is no hardcoded `SetMinSize` call anywhere in the class hierarchy; the earlier "likely a hardcoded ResizeTo/SetMinSize call" phrasing in this doc was a guess and is WRONG. The element `ResizeTo` is called against is not `0x2100002E`'s own root. Per `docs/research/2026-07-17-retail-shared-main-panel-pseudocode.md` (lines ~83-107) and the slot table in `docs/research/2026-08-11-fa-panel-structure.md` (row for `0x1000018E`), retail's Character/Skills tab content is one child slot inside the SHARED `gmPanelUI` host, LayoutDesc `0x2100006E`: ```text gmPanelUI host 0x100005FE 310 x 372 content parent 0x10000180 300 x 362 (anchored all edges) Character/Skills slot 0x1000018E panel id 11, 300 x 362 top-center Dragbar 0x1000065C Type 2 bottom-center Resizebar 0x10000660 Type 9 ``` `gmPanelUI::ResizeTo @0x004BC6E0` is a bare tailcall into `UIElement::ResizeTo` — the HOST is what gets resized (via its Resizebar), not `0x2100002E`'s content root; the content root's own absent MinHeight/MaxHeight (confirmed above) is therefore consistent with retail's actual mechanism, not evidence of a missing DAT property. **NOT PROBED by CT1.** The host elements above (`0x100005FE`, `0x10000180`, `0x1000018E`, `0x1000065C`, `0x10000660`) were read from the cited pseudocode doc, not re-probed live against the installed DAT this session. CT6's first step is to probe those host slots directly (min/max + resize authoring) before porting anything, then read `UIElement_Resizebar::StartMouseResizing @0x0046B7E0` verbatim for the drag-time clamp application. **Size tension for CT3/CT6 to resolve.** `0x2100002E`'s own root is authored **300×600** (the Titles page alone is 300×575, plus the 25px tab bar = 600 — §3), but retail mounts that content into the host's **300×362** slot (`0x1000018E`). CT3's title ListBox height (`0x10000532` is 270×455 per §3) and CT6's resize contract both assume a taller available area than the host slot's authored 362px. This doc does not resolve which number governs at runtime (scroll-clipped content inside a fixed slot vs. the slot itself growing to accommodate) — CT6 must resolve it from the decomp before implementing the resize contract, not infer it from either number in isolation. ## 5. The title-string table (DAT ground truth for `CharacterTitleTable::GetCharacterTitleFromID`) ### Decomp chain (named-retail, `docs/research/named-retail/acclient_2013_pseudo_c.txt`) `CharacterTitleTable::GetCharacterTitleFromID @0x005c6ed0` does **not** read a StringTable directly. It goes through TWO independent enum-mapper indirections: 1. `EnumMapper::GetString(0x10000006, titleId, &rawName)` (the static 3-arg overload @`0x0041ac40`) — internally calls `DBObj::GetDIDByEnum(&did, 0x10000006, /*category*/1)` to resolve the **title EnumMapper object's DID**, then dispatches on `MasterDBMap::DivineType` (0x24 = EnumMapper) to call `EnumMapper::GetString(titleId, &rawName)` on it, giving a raw canonical string name (NOT yet localized/hashed). 2. `compute_str_hash(rawName)` (ELF-style hash, already ported byte-exact as `DatStringResolver.ComputeHash` — see its own citation of `compute_str_hash @ 0x00413110`). 3. `StringInfo::SetStringIDandTableEnum(&info, hash, 0x10000007)` (`@0x0042c760`) — internally calls `DBObj::GetDIDByEnum(&did, 0x10000007, /*category*/4)` to resolve the **title StringTable's DID**. 4. `StringInfo::GetString(&info)` (`@0x0042e760` → `InqString` → `StringTableMetaLanguage::UnescapeString`) resolves the final localized text — the same `StringTable.Strings[hash]` lookup `DatStringResolver.Resolve(tableId, stringId)` already performs. `DBObj::GetDIDByEnum(enumValue, category)` (`@0x004153a0` → `DBCache::GetDIDFromEnum @0x00413940`) is itself a **two-level indirection** through a master map object (`this->m_MasterMapID`): look up `category` in the master map to get an intermediate category-map DID, then look up `enumValue` in THAT map to get the final DID. This is the exact same mechanism already ported (empirically, not by name) as `RetailKeyNames`' `0x2300000A`/`0x2300000B`/`0x23000007` constants (`src/AcDream.App/UI/Layout/RetailKeyNames.cs`, citing "`DBCache::GetDIDFromEnumStatic` category 4") — this slice confirms those three constants ARE exactly the category-4 (STRINGTABLE) map's enum 4/5/3 entries (see table below), so the existing `RetailKeyNames` port is independently cross-validated by this investigation. ### Live-DAT resolution (verified end-to-end this session) **The master-map / category-map tables and the `RetailKeyNames` cross-validation below are UNPINNED probe output** — they come from the same deleted `Assert.Fail` probe tests as the rest of this doc and are not backed by a committed `InstalledDatFact` assertion (unlike the `TitleStringTable_ResolvesWarMageEndToEnd` pin, which DOES commit the final two DIDs and the end-to-end string resolution). Treat the category-4 dump and the `RetailKeyNames` match column as this session's observation, re-derivable from the DAT but not regression-guarded. `DatReaderWriter.DBObjs.EnumIDMap` (ACE's historical name: `DidMapper`, file-type byte `0x25`) is the object type both master and category maps use; `DatReaderWriter.DBObjs.EnumMapper` (file-type byte `0x22` on the installed dat) is the flat id→string table type. **Master map, DID `0x25000000`** (`ClientEnumToID`/`ClientEnumToName`, 22 entries) — the categories relevant here: | category enum | name | category-map DID | |---|---|---| | 1 | EMAPPER | 0x25000001 | | 4 | STRINGTABLE | 0x25000004 | **Category 1 (EMAPPER) map, DID `0x25000001`** — relevant entry: | enum | name | DID | |---|---|---| | 0x10000006 | CharacterTitle | **0x22000041** | **Category 4 (STRINGTABLE) map, DID `0x25000004`** — full dump (12 entries), confirming the `RetailKeyNames` constants along the way: | enum | name | DID | cross-check | |---|---|---|---| | 0x00000003 | KeyMap | **0x23000007** | = `RetailKeyNames.DelimiterTableId` ✓ | | 0x00000004 | KeyNameOverride | **0x2300000A** | = `RetailKeyNames.KeyNameTableId` ✓ | | 0x00000005 | MetakeyNameOverride | **0x2300000B** | = `RetailKeyNames.MetaKeyNameTableId` ✓ | | 0x10000007 | CharacterTitle | **0x2300000E** | (this slice's target) | | 0x10000001 | UI | 0x23000001 | | | 0x10000002 | UI_Pregame | 0x23000002 | | | 0x10000003 | Preference | 0x23000003 | | | 0x10000004 | UI_Options | 0x23000004 | | | 0x10000006 | Options | 0x2300000D | | | 0x00000002 | Calendar | 0x23000006 | | | 0x00000006 | CommandSetup | 0x2300000C | | | 0x00000007 | ActionDescription | 0x23000005 | | | 0x00000008 | ServerEngine | 0x23000010 | | So: **the title EnumMapper is DID `0x22000041`; the title StringTable is DID `0x2300000E`.** ### End-to-end verification (ACE's `CharacterTitle` enum, `WarMage = 13`) ``` EnumMapper(0x22000041).IdToStringMap has 873 entries, including: titleId 0 -> ID_CharacterTitle_Invalid titleId 1 -> ID_CharacterTitle_Adventurer titleId 5 -> ID_CharacterTitle_Life_Mage titleId 13 -> ID_CharacterTitle_War_Mage titleId 14 -> ID_CharacterTitle_Wayfarer ComputeHash("ID_CharacterTitle_War_Mage") = 0x0543AF05 DatStringResolver(dats).Resolve(0x2300000E, 0x0543AF05) = "War Mage" ``` Byte-exact confirmation the chain is understood correctly end to end — titleId 13 round-trips through the EnumMapper canonical-name lookup, the retail hash function, and the StringTable localization lookup to produce exactly "War Mage". ### What CT2 needs to port 1. Two `GetDIDByEnum`-shaped lookups (master map `0x25000000` → category map → target DID) — CT2 can either hardcode the two resolved DIDs (`0x22000041` for the EnumMapper, `0x2300000E` for the StringTable, the way `RetailKeyNames` hardcodes its three) or port the two-level indirection generically. **A THIRD consumer has now appeared** (found this fix round): `gmAttributeUI::PostInit @0x0049DB70` resolves per-attribute icon DIDs via `DBObj::GetDIDByEnum(statEnum, category 0x10000002)` — see the "Row instantiation + icon-DID anchors" note in §2. With `RetailKeyNames` (category 4) and the title chain (categories 1 and 4) already hardcoding resolved DIDs, this third independent category (`0x10000002`) is the point where CT2/CT5 should factor a shared `GetDIDByEnum(enumValue, category)` helper instead of adding a fourth ad-hoc hardcoded pair. 2. `EnumMapper.IdToStringMap[titleId]` → raw canonical name (already readable via `dats.Portal.TryGet`). 3. `DatStringResolver.ComputeHash(rawName)` (already exists, no new code). 4. `DatStringResolver.Resolve(0x2300000Eu, hash)` (already exists, no new code) for the final localized display string. No new DAT-reading primitives are required — `EnumMapper`/`EnumIDMap` are already exposed by `DatReaderWriter.DBObjs`, and `DatStringResolver` already does steps 3–4 for other consumers. ## Corrections to the plan (summary) 1. **Window constraints are NOT already 90% wired — and the clamp mechanism is now VERIFIED, not guessed.** The plan's "Already in-tree" bullet claims `DatConstraintSource` registration for the character window; the actual `MountCharacter()` call sets no such field, and the DAT layout itself authors no MinHeight/MaxHeight on its root to source one from even if it were wired. `UIElement::ResizeTo @0x00463C30` clamps only via element attributes `0x3C`–`0x3F`, which nothing writes at runtime — the clamp source is always authored DAT properties on the resized element, full stop, and the resized element is the SHARED `gmPanelUI` host (`0x2100006E`, slot `0x1000018E`), not `0x2100002E`'s own root. See §4's "Verified resize mechanism" for the full chain and the unresolved 300×600-vs-300×362 size tension CT6 must still resolve. 2. **The row-template elements are not walkable via the normal `ImportInfos(dats, layoutId)` overload.** `#375`'s prototype-skip logic deliberately excludes same-layout template-list targets from the built tree. CT5 must use `ImportInfos(dats, layoutId, elementId)` (the targeted single-root overload) to read `0x10000248` (`LayoutDesc 0x21000045`) and `0x10000536` (`LayoutDesc 0x2100005E`) — documented here so CT5 doesn't waste a cycle rediscovering the same "NOT FOUND" dead end this slice hit first. 3. **`RowHighlightSprite` IS wrong — SEALED, not merely flagged.** The DAT's row-template Highlight state is `0x06000F93`, reached via `gmAttributeUI::UpdateSelection`'s `SetState(6)` → `InfoRegion::SetState` on the row itself; the current constant in `CharacterStatController.cs` is `0x06001397`, which belongs to a DIFFERENT mechanism (the spellbook row's selected-overlay child, `UIElement_UIItem::SetSelectedState`). CT5 must correct the STAT rows' `RowHighlightSprite` to `0x06000F93` and must NOT touch `SpellbookRowStyle.cs` — see §2's "SEALED VERDICT" note for the full anchor chain. 4. Everything else in the plan's "Retail recon" section (the Titles page element roster, the header element ids, the PostInit binding order) checks out exactly against the live DAT — no other corrections. 5. **Several findings above are probe-session observations, not committed pins** — flagged this fix round so CT2–CT6 don't cite them as regression-guarded facts: the header block's "two copies geometrically identical" claim (only fonts/colors are pinned, not full geometry — §1), the `0x06004CC2` "generic panel chrome" characterization (§2), and the master-map/category-map dump plus the `RetailKeyNames` cross-validation table (§5). The title chain's final two DIDs and end-to-end string resolution ARE pinned (`TitleStringTable_ResolvesWarMageEndToEnd`).