# Campaign OP slice OP2 — MECHANISM-FAITHFULNESS review **Date:** 2026-08-11 **Commit under review:** `df9c7a35` — "feat(ui): Campaign OP slice OP2 — tab control, template ListBox, UIOption widget mappings" **Lens:** retail-mechanism fidelity + contract fidelity against `docs/plans/2026-08-10-options-panel-campaign.md` §4 OP2 and `docs/research/2026-08-10-options-panel-structure.md`. **Mode:** READ-ONLY. No build, no test run, no launch. Every claim below is either a quote from committed source, a decode of a committed fixture, or an extract from `docs/research/named-retail/acclient_2013_pseudo_c.txt`. ## VERDICT: **REJECT** Two independent MUST-FIX classes, each with hard evidence: 1. **The unconditional `Type 8` mapping (and the `Type 5` mapping) silently rewires four already-shipped windows and nine already-shipped list boxes at runtime.** The committed test suite cannot see it because the 27 pre-existing fixtures were deliberately NOT regenerated, so they deserialize with `TabTable`/`TemplateList` empty. One already-committed test (`EffectsUiControllerTests.cs:135-137`) encodes the pre-OP2 contract and would fail against real DAT data. The slice's own acceptance criterion ("no change to any existing widget's behaviour") is false as landed. 2. **The retail mechanism for both new widgets is fully present in the named decomp and was not consulted.** `UIElement_Panel::Update/OpenTab/SetupTabPageHash` (Type 8 is `UIElement_Panel`, **not** a class called `UIElement_TabControl` — that class does not exist in the PDB) and `UIOption_CheckboxBitfield64::CreateChildren/ListenToElementMessage/ Refresh` are all decompiled. Reading them confirms most of the port, refutes `UiCheckboxBitfield64`'s checked-state predicate, refutes its "the dat authors nothing" premise, and makes TS-72's stated unknown a non-unknown. The parts the prompt asked me to independently verify — the two structural identities, the tab-table decode, the template-list decode, the effective-state discipline, the conformance pins — are **all correct**. This is a REJECT on blast radius and on two mechanism defects, not on the Options-panel data work, which is clean. --- ## 1. Property readers (0x2E / 0x64 / 0x72) ### 1.1 Tab table `0x2E` — decode CORRECT, entry filtering DIVERGES `ElementReader.ReadTabTable` (`src/AcDream.App/UI/Layout/ElementReader.cs:508-527`) decodes `{0x30 button, 0x31 page, 0x32 isDefault}` and preserves authored array order. Independently decoded from the committed fixture `tests/AcDream.App.Tests/UI/Layout/fixtures/options_2100002B.json`, element `0x10000208` DirectState: ``` P0x58 = Enum(0x1) P0x2E = Array[4] Struct{0x30=Enum(0x1000020D), 0x31=Enum(0x10000212), 0x32=Bool(True)} Struct{0x30=Enum(0x1000020E), 0x31=Enum(0x10000211), 0x32=Bool(False)} Struct{0x30=Enum(0x1000050B), 0x31=Enum(0x1000050C), 0x32=Bool(False)} Struct{0x30=Enum(0x1000020F), 0x31=Enum(0x10000213), 0x32=Bool(False)} P0x57 = Enum(0x1000001A) ``` This matches research §1.3 / §10.1 exactly (Gameplay default, entry 0; Chat third at `0x1000050B`/`0x1000050C`). The `Enum`/`DataId`/`Integer` coercion in `ReadStructMemberId` (`:554-564`) is the right tolerance — the authored kind here is `Enum`. Retail's own reader is `UIElement_Panel::SetupTabPageHash @0x0046C2E0`. Three filters acdream does not implement: | Retail | acdream | Impact | |---|---|---| | `BaseProperty::GetPropertyName(entry) != 0x2F` → **skip entry** (`0x0046C3A6`) | any `Struct` accepted | NOTE — every authored entry is `0x2F` | | `InqProperty(0x30)` or `InqProperty(0x31)` missing → **skip entry** (`0x0046C3E5`, `0x0046C3FC`) | entry added with `0` for the missing member | **SHOULD-FIX** — a partial entry yields `UiTabTableEntry(0, pageId, …)` that still participates in `SwitchTo` and can hide a page | | `tab != page` required (`0x0046C46B`) | not checked | NOTE | ### 1.2 Template list `0x64` — decode EXACT `ReadTemplateList` (`:529-545`) decodes `{0x63 layout DID, 0x62 element id}`, in that order. Retail: `UIElement_ListBox::AddItem(BaseProperty*) @0x0046F170` reads `InqStructElement(0x63)` then `InqStructElement(0x62)` and calls `AddItem(did, elementId, parent)`. **Byte-for-byte the same pair, same order.** ✔ Decoded from the committed fixtures (all three arrays, verbatim): ``` 0x100001FA (Character) P0x64 = 3 × {0x2100002B, 0x10000216 / 0x10000217 / 0x10000218} 0x10000200 (Config) P0x64 = 8 × {0x2100002B, 216,217,218,21A,222,220,21D,221} 0x1000050D (Chat) P0x64 = 9 × {…same 8…, 0x10000520} ``` Identical to research §1.5's table. ✔ ### 1.3 Scrollbar `0x72` — correct value, imprecise semantics, missing sibling `ReadReferencedElementId(info, 0x72)` (`:569-579`) is correct: the authored kind is `Enum` and retail reads it with `InqEnum`. Exact retail semantics, from `UIElement_Scrollable::OnSetAttribute @0x00474630` (not cited anywhere in the commit): ``` 0x71 -> m_eHorizonalScrollbarID (+ UpdateScrollbarSize_(1)) 0x72 -> m_eVerticalScrollbarID (+ UpdateScrollbarSize_(0)) ``` resolution is `UIElement::FindRelative(this, id)` validated by `DynamicCast(0xB)` and a mutual-relative check (`GetScrollbarPointer_ @0x00473EC0`). - **NOTE:** `ElementInfo.ScrollbarElementId` / `UiTemplateListBox.ScrollbarElementId` should be named *vertical*; `0x71` (horizontal) is unread. `0x71` IS authored in already-committed fixtures (`vendor_…` `0x100000BD`/`0x100000C5`/`0x100000CE`, `combat_…` `0x100000B6`) — not on the Options ListBoxes, so no OP2-observable gap, but the asymmetry deserves a comment. ### 1.4 The round-5 N1 lesson — CORRECTLY OBSERVED All three readers resolve through `ElementInfo.TryGetEffectiveProperty` (`:215-235`), which is DirectState-as-base then effective-default override — **not** a per-state first-wins scan. `ApplyCanonicalLegacyProjection` is invoked from both entry points (`LayoutImporter.ToInfo` and `ElementReader.Merge:410`), and the three new fields are *recomputed* from the merged state rather than scalar-merged, which is strictly correct. No N1 reintroduction. ✔ - **SHOULD-FIX (coverage):** the new conformance tests read `TabTable`/`TemplateList`/ `ScrollbarElementId` off the **serialized** fixture fields — `LayoutImporter.Build` does not re-run the projection at load. So `ReadTabTable`/`ReadTemplateList` are exercised only at fixture-generation time and a future regression in either reader would not fail a single test. One synthetic `ElementInfo` + `States[Direct].Properties[0x2E]` unit test per reader closes this for ~20 lines. --- ## 2. `UiTabControl` — right behaviour, wrong class, wrong lookup, invented fallback **The core behaviour is confirmed against retail.** `UIElement_Panel::Update @0x0046BD00`: ``` if (arg2 != m_OpenPageToken && arg3 != m_OpenTabToken) { m_OpenTabToken = arg3; m_OpenPageToken = arg2; foreach (tabId -> pageId) in m_TabPageHash { visible = 0; state = 0xB; // Closed if (m_OpenTabToken == tabId && m_OpenPageToken == pageId) { visible = 1; state = 0xC; } // Open page = GetChildRecursive(this, pageId); if (page) page->SetVisible(visible); tab = GetChildRecursive(this, tabId); if (tab) tab->SetState(state); } UIElement::BroadcastElementMessage(this, 0x2C, 0, 0); } ``` `UiTabControl.SwitchTo` (`src/AcDream.App/UI/UiTabControl.cs:88-103`) does exactly this: exactly one page `Visible`, every tab button driven to `Open`/`Closed` via `RetailTabBinding.SetOpen` → `RetailUiStateIds.Open = 12` / `Closed = 11` (`src/AcDream.App/UI/IUiDatStateful.cs:15-16`) = retail's `0xC`/`0xB`. ✔ The authored data corroborates independently: the four tab buttons are `Type 0xC` text elements with `DefaultStateId = 11 (Closed)` and authored state-11 grey `ARGB(255,127,127,127)` vs state-12 `ARGB(255,204,204,204)` font colours. **The authored default IS honoured on mount** — `OnChildrenAttached` (`:65-81`) picks the `IsDefault` entry and `SwitchTo`s it, and the fixture proves entry 0 (Gameplay, `0x1000020D`/`0x10000212`) is the sole default. Retail takes the last `0x32==true` entry; acdream's loop does too. ✔ Four defects: - **MUST-FIX (citation).** The class is documented as retail `UIElement_TabControl` (`UiTabControl.cs:7`, `DatWidgetFactory.cs:108-109`). **No such class exists** — `grep UIElement_TabControl docs/research/named-retail/symbols.json` returns nothing. Type 8 is `UIElement_Panel` (`UIElement::RegisterElementClass(8, UIElement_Panel::Create)` at `0x0046C6B7`). CLAUDE.md's phase checklist requires a named symbol + address; citing a fictional class is what let items 2–4 below through. Correct anchors: `UIElement_Panel::SetupTabPageHash @0x0046C2E0`, `::Update @0x0046BD00`, `::OpenTab @0x0046BE20`, `::InqTabFromPage @0x0046BEB0`, `::ListenToElementMessage @0x0046BF90`. - **SHOULD-FIX.** `FindDirectChild` (`:105-111`) scans immediate children only; retail uses `GetChildRecursive` for **both** the tab and the page. Fine for `0x2100002B` (all direct), wrong for the Configure Keyboard tab host `0x1000049B` (research §8) this widget is explicitly meant to also serve. - **SHOULD-FIX.** `defaultEntry ??= _tabs[0]` (`:78`) is invented. Retail leaves `m_OpenPageToken/m_OpenTabToken` at 0 when no entry authors `0x32`, so `Update(0,0)` hits the guard and **no switch happens at all**. Either match retail or file a register row. - **NOTE.** Retail's `BroadcastElementMessage(this, 0x2C, …)` after each switch, its `SetMouseVisible(1)` per tab button during setup (`0x0046C4FB`), and its `ListenToElementMessage` handling of `idMessage 0x18` (a page becoming visible re-opens its tab; the panel follows its open page's visibility) are unported. Retail also drives switching from a child element message (`0x19`/`0x29` → `OpenTab`) rather than a per-button click delegate — acdream's `RetailTabBinding.SetClick` adaptation is consistent with the existing character/vendor/spellbook tabs, but it is an adaptation and OP3 will need the `0x2C` notification for the `OnVisibilityChanged` apply/revert semantics (research §3.6). ### 2.1 **MUST-FIX — the Type-8 arm is unconditional and rewires four shipped windows** `DatWidgetFactory.cs:110` maps **every** Type-8 element to `UiTabControl`. Type 8 = `UIElement_Panel`; a Panel without a `0x2E` array is just a panel. Two consequences, both regressions relative to the previous `_ => new UiDatElement(info, resolve)` fallback: * `UiTabControl : UiElement` has **no `OnDraw`** — the authored DirectState media is not drawn (`UiDatElement.OnDraw`, `src/AcDream.App/UI/Layout/UiDatElement.cs:193-216`). * `UiElement.ClickThrough` defaults **false**; `UiDatElement` sets it **true** (`UiDatElement.cs:111`). Every converted panel now swallows clicks that miss a child. I decoded the raw `States` bag of every committed fixture (the raw property survives even though the projected field does not). Pre-existing Type-8 elements: | Fixture | Element | Size | Authored `P0x2E` | Media | Consequence | |---|---|---|---|---|---| | `vendor_21000012_100000B7.json` | `0x1000008D` | 800×20 | **none** | **yes** (`0x06004…`, DirectState) | loses its fill; `RetailUiRuntime.cs:2287` comments that this element "provides its own fill" | | `vendor_…` | `0x100000B8` | 800×110 | 3 entries (`B9/BC`, `BA/C4`, `BB/CD`) | no | live tab control fights `VendorUiController` | | `character_2100002E.json` | `0x10000227` (window root) | 300×600 | 3 entries (`228/22B`, `229/22C`, `538/539`) | no | live tab control; entry 3 is the **Titles** tab `CharacterStatController.cs:432` deliberately keeps inert | | `spellbook_21000034.json` | `0x100002A8` (window root) | 300×600 | 2 entries | no | live tab control vs `SpellbookWindowController` | | `combat_21000073.json` | `0x100000A2` | 800×60 | **8 entries** (`A3/AA` … `5C2/5C3`) | no | **no controller rebinds these** — `UiTabControl` takes ownership outright and hides 7 of the 8 stance pages | `VendorUiController`/`SpellbookWindowController`/`CharacterStatController` re-bind their tabs after import, so their click hijack is overwritten; but `UiTabControl.OnChildrenAttached` still runs first and mutates `Visible`/state, and **combat has no re-binder at all**. The minimum correct fix is (a) gate the arm exactly like the Type-5 arm (`8 when info.TabTable.Count > 0`) so media-bearing plain panels such as `0x1000008D` keep `UiDatElement`, (b) give `UiTabControl` `UiDatElement`'s media draw + `ClickThrough` semantics, and (c) explicitly reconcile the four pre-existing hosts — either `UiTabControl` owns their switching and the duplicated controller code goes, or Type 8 is not mapped outside `0x2100002B` in this slice. ### 2.2 **MUST-FIX — the same class of regression on the Type-5 arm** `DatWidgetFactory.cs:99-105` justifies the gate with "none currently reach this factory". That is refuted by the same raw-property decode. Pre-existing Type-5 elements that **do** author `P0x64`, and therefore flip from `UiDatElement` to `UiTemplateListBox` at runtime: | Fixture | Element | Media | Referenced by | |---|---|---|---| | `effects_positive/negative_2100001B.json` | `0x10000123` | no | `EffectsUiController.ListId` | | `examine_2100006B_100005F2.json` | `0x10000149`, `0x10000335`, `0x1000032D` | no | `AppraisalUiController.CreatureStatsListId` / `CreatureExtraListId` / `SpellFormulaListId` | | `character_2100002E.json` | `0x1000023D` | no | `CharacterStatController.ListBoxId` | | `character_2100002E.json` | `0x10000532` | **yes** | Titles page list | | `spellbook_21000034.json` | `0x10000464` | no | `SpellbookWindowController.ComponentListId` | | `mini_game_2100001E.json` | `0x10000174` | **yes** | mini-game board | `UiTemplateListBox : UiPanel` zeroes `BackgroundColor`/`BorderColor` (`UiTemplateListBox.cs:92-93`) and draws no authored media, is not `ClickThrough`, and inserts an anchored `UiScrollablePanel` as child 0. **Smoking gun:** `tests/AcDream.App.Tests/UI/Layout/EffectsUiControllerTests.cs:135-137` already asserts ```csharp UiElement listHost = Assert.IsType(layout.FindElement(EffectsUiController.ListId)); UiItemList list = Assert.IsType(Assert.Single(listHost.Children)); ``` Against real DAT data both assertions now fail (wrong type; two children). It passes only because `effects_positive_2100001B.json` predates the `TemplateList` field. **This is also why the commit message's fixture-drift paragraph is wrong.** It states the regenerated drift was "an Outline/OutlineColor field pair added by an earlier commit, bcc34ee3". The pre-existing fixtures contain **neither** `Outline`/`OutlineColor` **nor** `TabTable`/`TemplateList`/`ScrollbarElementId` (grep count 0 for all five in `character_2100002E`, `vendor_…`, `spellbook_…`, `combat_…`). Regeneration would have populated the three new fields too — i.e. the decision to revert the drift is exactly what suppressed the evidence of §2.1/§2.2. Regenerating those 27 fixtures is the acceptance gate for any fix here. --- ## 3. `UiTemplateListBox` — mechanism faithful `AddItemFromTemplateList(index)` (`src/AcDream.App/UI/UiTemplateListBox.cs:104-118`) resolves `Templates[index]` through `TemplateResolver(layoutId, elementId)` and appends the built subtree. Retail (`UIElement_ListBox::AddItemFromTemplateList @0x0046F2B0` → `AddItem(BaseProperty*) @0x0046F170` → `AddItem(did, elementId, parent) @0x0046EDC0` → `UIElementManager::CreateChildElementByDid(this, did, elementId)`) does the same thing: build the named element out of the named LayoutDesc as a child of the list box, then append. Instantiating through `LayoutImporter.ImportInfos(dats, layoutId, elementId)` + `Build` is the correct acdream analogue of `CreateChildElementByDid`, and it is the same overload `RetailDialogFactory` already uses. ✔ Out-of-range and no-resolver both return null, matching retail's null returns. ✔ Scrollbar linkage is carried as data (`ScrollbarElementId`) for a page controller to bind to `Scroll`; that mirrors `ChatWindowController`'s existing pattern. Acceptable for a "mechanism-only" slice — see §1.3 for the naming/`0x71` notes. - **NOTE.** Row stacking uses `_viewport.ContentHeight` as the next row's `Top` (`:114-116`). Retail sizes the box from `CalculatePaperSize` and the ListBox owns row placement; the acdream model is equivalent for uniform top-anchored rows and is the same model `CharacterStatController.RebuildActiveList` already uses. Fine. - **NOTE.** Authored dat children of a `UiTemplateListBox` land as siblings of the internal viewport (the class does not override `AddChild`), while template rows go inside it. Benign for the Options ListBoxes (no authored children) but relevant to §2.2's converted lists. --- ## 4. The two structural identities — **BOTH CONFIRMED** (independent check) I decoded the fixture myself rather than taking the commit message's word. ### 4.1 `UIOption_Slider 0x10000037` ≡ horizontal `UIElement_Scrollbar` — **CONFIRMED** `options_2100002B.json` element `0x1000021C`: ``` 0x1000021C T=0x10000037 (146,4) 120x12 DirectState image 0x060012C5 P0x7B=Bool(True) P0x86=Float(0.5) P0x7C=Bool(True) P0x7E=Bool(False) P0x82=Bool(False) P0x83=Bool(True) [+ P0x47/P0x48/P0x4B, see below] child id=1 T=0x1 7x12 DirectState image 0x060012C6 ``` Compare the real `Type 0xB` scalar slider in `combat_21000073.json`, element `0x1000004F` (gmCombatUI desired-power): ``` 0x1000004F T=0xB 707x14 P0x7B=Bool(True) P0x86=Float(0.5) P0x7C=Bool(True) P0x7E=Bool(False) P0x82=Bool(False) P0x83=Bool(False) child id=1 T=0x1 12x14 child id=4 T=0x3 child T=0x7 (meter) ``` **Identical property fingerprint, identical `child id = 1` thumb convention, `W > H`.** `BuildScrollbar`'s horizontal branch (`DatWidgetFactory.cs:223-279`) reads exactly `DefaultImage(info)` for the track and `child id 1` for the thumb. The identity holds; the mapping is correct and the conformance test (`OptionsPanelFixture_SliderControl_BuildsAs…`) pins `Horizontal == true` and a non-zero thumb sprite. ✔ ### 4.2 `UIOption_Menu 0x10000038` ≡ `UiMenu`'s shape — **CONFIRMED** ``` options_2100002B 0x10000224 T=0x10000038 120x18 P0x2 P0x6 P0x7=DID(0x21000043) P0x8 P0x9 P0xA=DID(0x21000043) P0x13 [+0x47/0x48/0x4B] children: 0x10000355 T=0xC 100x18 (label) 0x10000356 T=0x3 17x19 (arrow cap) vendor_… 0x100000BF T=0x6 (UIElement_Menu) 117x18 P0x2 P0x6 P0x7=DID(0x21000043) P0x8 P0x9 P0xA=DID(0x21000043) P0x13 children: 0x1000034D T=0xC 100x18 (label) 0x1000034E T=0x3 17x19 (arrow cap) ``` Same seven properties, the same popup-template DID `0x21000043`, the same 100×18 label + 17×19 arrow-cap child pair. `new UiMenu()` is right, and it matches how the existing Type-6 arm is built. ✔ - **NOTE (both identities).** The `UIOption_*` family carries an extra authored triple `P0x47=Enum(0x10000397)`, `P0x48=DID(0x21000041)`, `P0x4B=Bool(True)` that the plain Type-6/Type-11 counterparts do not. It is on the slider, the menu, **and** the already-mapped `UIOption_Checkbox 0x10000219`, so it is a pre-existing uniform gap rather than something OP2 introduced — but somebody should establish what `0x21000041/0x10000397` is (it looks like a shared tooltip/option-chrome base pair) before OP4–OP6 rely on those rows rendering completely. --- ## 5. `UiOptionToggleSlider` / `UiCheckboxBitfield64` and TS-72 ### 5.1 `UiOptionToggleSlider` — consistent with the authored templates ✔ Fixture-verified: `0x10000220` (272×20) and `0x10000221` (272×36) are both `T=0x10000036` and both contain exactly one `0x10000219` (`T=0x10000035`, checkbox) and one `0x1000021C` (`T=0x10000037`, slider); `0x10000221` additionally carries the two range-label `Type 0xC` children `0x1000021E`/`0x1000021F`. Composing rather than consuming children is right, and `OnChildrenAttached` (`src/AcDream.App/UI/UiOptionToggleSlider.cs:33-42`) picks the first `UiButton` and first `UiScrollbar`, which is unambiguous for both templates. The "labelled slider row `0x1000021D` is a plain `Type 3`, not `0x10000036`" test is a genuinely good data-driven discriminator. ✔ ### 5.2 `UiCheckboxBitfield64` — **MUST-FIX ×2** **(a) The checked-state predicate is wrong.** `IsSet` (`src/AcDream.App/UI/UiCheckboxBitfield64.cs:137-138`) requires **all** mask bits set. Retail `UIOption_CheckboxBitfield64::Refresh @0x004859C0`: ``` lowHit = m_current.low & mask.low; highHit = m_current.high & mask.high; allSet = (lowHit == mask.low && highHit == mask.high); // computed… ebx = ((lowHit | highHit) != 0) ? 0 : 1; // …but the VISIBLE flag is ANY-set row = UIElement_ListBox::GetItem(this, i); cb = UIElement::GetChild(row, 0x10000219); UIElement::SetAttribute_Bool(cb, 0xE, ebx == 0); // checked <=> ANY mask bit set ``` Retail checks the box when **any** bit of the mask is set; acdream only when **all** are. This matters immediately: 8 of the 13 Chat filter masks are multi-bit (Gameplay `0x83912021`, Combat `0x00600040`, Magic `0x00020080`, AreaSpeech `0x00001004`, Tells `0x00000018`, Allegiance `0x00040C00`, …). The click direction is derived from the displayed state on both sides, so a wrong predicate inverts the next click too. **(b) The toggle math is NOT unknown — TS-72's premise is false.** `UIOption_CheckboxBitfield64::ListenToElementMessage @0x00485AE0`: ``` if (idMessage == 1 && GetAttribute_Int (pElement, 0x10000084, &index) // row index stamped at build time && GetAttribute_Bool(pElement, 0xE, &onOff) // the row checkbox's new state && index < childCount) { BitUtils::SetBitsOnOrOff(&m_current, m_rgChildValues[index].mask, onOff); vtable+0x2D4 (1); // Apply(1) } return UIElement_ListBox::ListenToElementMessage(this, edi); ``` `SetBitsOnOrOff(value, mask, on)` **is** "on ⇒ OR mask, off ⇒ AND-NOT mask" — so `ToggleRow` (`:140-155`) is *correct*, and TS-72's claim that "the decompiled excerpt this campaign pulled covers `Apply`'s WRITE side, not its own click-handler's bit math" describes what the campaign grepped, not what exists. Every method is in `acclient_2013_pseudo_c.txt` and `symbols.json`: `CreateChildren @0x00485DF0`, `AddChild @0x004862A0`, `Refresh @0x004859C0`, `ListenToElementMessage @0x00485AE0`, `GetValue @0x00486150`, `Changed/SaveCurrentValue/RestoreSavedValue/RestoreDefaultValue` `@0x004856A0…0x00485780`. **TS-72 is therefore not an accurate statement of what is and is not verified.** It should be rewritten (or retired) to say: the OR/AND-NOT half is byte-confirmed correct; the checked-state predicate is byte-confirmed **wrong**. **(c) "The template authors zero children" is refuted by the committed fixture.** `BuildCheckboxBitfield64` (`DatWidgetFactory.cs:810-815`) reads nothing from `ElementInfo`, and the class doc (`UiCheckboxBitfield64.cs:11-14`) plus the commit message assert the template "carries NO children and NO media". The actual authored element: ``` 0x10000520 T=0x10000044 272x100 P0x64 = Array[1] -> Struct{0x63=DID(0x2100002B), 0x62=Enum(0x10000521)} P0x10000082 = DID(0x06004D17) P0x10000083 = DID(0x06004D19) ``` It authors **its own row-template list** — the very `0x64` property this commit taught the reader to decode — naming `0x2100002B/0x10000521`, plus two RenderSurface DIDs (which are the LED sprite's `Highlight`/`Ghosted` images, `0x06004D17`/`0x06004D19`). `0x10000521` is a 272×20 container holding `0x10000219` (`Type 1`, base `0x21000044/0x10000329`) with the 13×13 five-state LED child `0x10000328` — i.e. exactly the row art. Research §1.5 flagged `0x10000521` as "UNKNOWN consumer"; **this fixture closes that unknown** (research U10 too), and the decomp confirms it: ``` UIOption_CheckboxBitfield64::CreateChildren @0x00485DF0 for i in 0 .. m_rgChildValues.m_num-1: row = UIElement_ListBox::AddItemFromTemplateList(this, 0, nullptr); // template idx 0 cb = GetChildRecursive(row, 0x10000219)->DynamicCast(1); SetAttribute_Int(cb, 0x10000084, i); UIElement_Text::SetStringInfo(cb, childValues[i].siLabel); UIElement::SetTooltip (cb, childValues[i].siTooltip); ResizeTo(GetWidth(), CalculatePaperSize(0, -1)); Refresh(); ``` `UIOption_CheckboxBitfield64` **is a `UIElement_ListBox`** — it calls `UIElement_ListBox::AddItemFromTemplateList`/`GetItem`/`CalculatePaperSize`/ `ListenToElementMessage` on itself. So the faithful acdream port is `UiCheckboxBitfield64` composing the `UiTemplateListBox` mechanism **this same commit shipped**, not a hand-rolled `UiPanel` of synthesized `UiButton`s. As written, `AddChild` (`:108-135`) fabricates an `ElementInfo{Type=1, Height=RowHeight}` with a hand-injected `0x0B` property and an invented `RowHeight = 14f` whose comment claims "No authored row height exists for this block (it is unauthored in the dat)" — the dat authors 272×20 with a 260×14 checkbox inset at (6,2). Consequences as landed: no LED art, no authored font (`0x40000002`), no authored colour (`0x1B`), no authored label insets (`0x23`/`0x25`), no justification (`0x14=Left`), no tooltip plumbing, and no outline seeding. `AddChild(uint64 mask, uint32 labelId, uint32 tooltipId) @0x004862A0` is also confirmed to be pure bookkeeping (append a `ChildInfo{mask, StringInfo(label, tableEnum **0x10000006**), StringInfo(tooltip, 0x10000006)}`) — note the table enum is `0x10000006`, not the `0x10000003` used for headers/labels elsewhere. Worth pinning for OP5. --- ## 6. Outline seams (`aa6635ae` / AP-192) — **PRESERVED** I scanned all five new fixtures for authored `0x21`/`0x22`. Every occurrence (32 total) is `0x21 = Bool(True)` **in state 3 only**, on `Type 1` buttons: the three Apply/Reset/Defaults buttons on each of Character/Chat/Config, and the seven Gameplay-tab buttons. Those build through the untouched `BuildButton`, whose effective-default resolution correctly yields outline-off — precisely the AP-192 deferral, unchanged. No authored `0x22` anywhere; no authored `0x21` on any element reaching a *new* build path. ✔ - **NOTE.** `new UiMenu()` for `0x10000038` does not seed `Outline`/`OutlineColor` — but neither does the pre-existing Type-6 arm, and `UiMenu`'s own doc records "No authored menu element carries it today", which the Options menu `0x10000224` confirms (no `0x21`/`0x22` on it or its label child). Unchanged, not regressed. - **NOTE (folds into §5.2c).** `UiCheckboxBitfield64`'s synthesized rows have no outline seeding either — but that is a symptom of bypassing the authored template, not a separate defect. Building the rows through `AddItemFromTemplateList` gets the seam for free via `BuildCheckbox`. --- ## 7. Conformance tests vs research §10.1 — **ACCURATE**, coverage thin in two places I spot-checked every pinned constant against the research doc and the fixtures. All correct: | Test | Pins | Research §10.1 / §1.3 / §1.5 | Verdict | |---|---|---|---| | `…TabControl_HasFourEntriesInAuthoredOrder_GameplayDefault` | `020D/0212 default`, `020E/0211`, `050B/050C`, `020F/0213`, `Assert.Single(IsDefault)` | identical | ✔ | | `CharacterListBoxFixture_TemplateList_…` | 3 × `{0x2100002B, 216/217/218}` | identical | ✔ | | `ConfigListBoxFixture_TemplateList_…` | 8, order `216,217,218,21A,222,220,21D,221` | identical | ✔ | | `ChatListBoxFixture_TemplateList_…` | 9, + `0x10000520` | identical | ✔ | | scrollbar linkage ×3 | `0x100001FB`, `0x10000201`, `0x10000201` | identical | ✔ | | `CharacterListBox_TemplateMechanism_ReachesSixHeadersAndFortyNineToggles` | `{3,15,6,11,7,7}` = 49 under 6 headers; `6*22 + 49*20` content height | §7 counts; template heights 292×22 / 272×20 confirmed in fixture | ✔ | | `UiTabControl_*` ×5 | default mount, exactly-one-visible, Open/Closed flip, click-through, no-op | matches `UIElement_Panel::Update` | ✔ | Two gaps: - **SHOULD-FIX.** No test pins the reader itself (see §1.4) — every "reader" test actually reads a serialized field. - **SHOULD-FIX.** `UiCheckboxBitfield64_AddChild_TracksMaskAndTogglesOnClick` deliberately picks the single-bit `Error` mask (`0x04000000`) because it "round-trips cleanly" — which is exactly the case that cannot distinguish acdream's all-set predicate from retail's any-set predicate. A multi-bit mask (Combat `0x00600040`) against a partially-set current value is the test that would have caught §5.2a. - **NOTE.** The plan's OP2 contract cites "tab-button ↔ page-slot switching per lane A §5". §5 is the Chat-tab row map; the tab control is §1.3 and its host is §1.4. The implementer cited the right sections; the plan's cross-reference should be corrected so OP8 does not chase §5. --- ## Summary of classifications **MUST-FIX** 1. `DatWidgetFactory.cs:110` — unconditional `Type 8 → UiTabControl` converts four shipped Type-8 hosts (vendor `0x100000B8`, character `0x10000227`, spellbook `0x100002A8`, combat `0x100000A2` — the last with **no** controller re-binder) and strips `UiDatElement` media drawing + `ClickThrough` from the media-bearing vendor backdrop `0x1000008D`. 2. `DatWidgetFactory.cs:104` — the Type-5 gate's "none currently reach this factory" is false; nine shipped ListBoxes author `P0x64` (effects, examine ×3, character ×2, spellbook, mini-game). `EffectsUiControllerTests.cs:135-137` already encodes the pre-OP2 contract and fails against real DAT data. Fixture staleness — not correctness — is what keeps the suite green; regenerating the 27 fixtures is the acceptance gate for the fix. 3. `UiCheckboxBitfield64.cs:137-138` — checked-state predicate is all-bits-set; retail (`Refresh @0x004859C0`) is any-bit-set. 4. `UiCheckboxBitfield64` / `DatWidgetFactory.cs:810` — the template `0x10000520` **does** author content (`P0x64 → 0x2100002B/0x10000521`, plus `P0x10000082`/`P0x10000083`); retail `CreateChildren @0x00485DF0` builds every row through `AddItemFromTemplateList(0)`. The synthesized `ElementInfo` + invented `RowHeight = 14f` must be replaced by the authored template path, and the false "unauthored in the dat" comments removed. 5. `UiTabControl.cs:7` / `DatWidgetFactory.cs:108` — cite `UIElement_Panel` (`SetupTabPageHash @0x0046C2E0`, `Update @0x0046BD00`, `OpenTab @0x0046BE20`); the named class `UIElement_TabControl` does not exist in the PDB. 6. `docs/architecture/retail-divergence-register.md` TS-72 — inaccurate as written; the click math is in the decomp, the OR/AND-NOT half is confirmed right and the predicate half is confirmed wrong. Also missing rows for the invented bitfield row geometry and the dropped `0x64`/`0x10000082`/`0x10000083` authored data. **SHOULD-FIX** 7. `ReadTabTable` must skip entries missing `0x30`/`0x31` (retail `0x0046C3E5`/`0x0046C3FC`). 8. `UiTabControl.FindDirectChild` → recursive descendant lookup (retail `GetChildRecursive`). 9. `defaultEntry ??= _tabs[0]` is invented; retail performs no switch when nothing authors `0x32`. 10. Add reader-level unit tests for `0x2E`/`0x64`/`0x72` (fixture tests only pin serialized values). 11. Add a multi-bit-mask `UiCheckboxBitfield64` test. **NOTE** 12. `0x2F` MasterPropertyId and `tab != page` filters unimplemented (harmless today). 13. `0x72` is specifically the **vertical** scrollbar (`UIElement_Scrollable::OnSetAttribute @0x00474630`); `0x71` (horizontal) is unread though authored in vendor/combat layouts. 14. `BroadcastElementMessage(0x2C)`, `SetMouseVisible(1)`, and the `idMessage 0x18` visibility-follow path are unported — OP3 needs the first for §3.6 semantics. 15. The `UIOption_*` `P0x47/P0x48/P0x4B` triple (`0x21000041/0x10000397`) is unread across the whole family, including the pre-existing checkbox mapping. 16. `UIOption_CheckboxBitfield64::AddChild` uses string table enum `0x10000006`, not `0x10000003` — pin for OP5. 17. Research §1.5's "`0x10000521` … UNKNOWN consumer" and §10.5 U10 are now closed by the committed fixture; the research doc should be updated. 18. The plan's OP2 contract cites "lane A §5" for the tab host; the correct sections are §1.3 (tab table) and §1.4 (host). 19. The commit message's fixture-drift paragraph understates the drift (three new fields, not one pair). --- ## Appendix — retail extracts used (all from `docs/research/named-retail/`) | Symbol | Address | Used for | |---|---|---| | `UIElement::RegisterElementClass(8, UIElement_Panel::Create)` | `0x0046C6B7` | Type 8 identity | | `UIElement_Panel::SetupTabPageHash` | `0x0046C2E0` | `0x2E` entry filters, default selection, `Update(page, tab)` tail | | `UIElement_Panel::Update` | `0x0046BD00` | one-visible-page + `SetState(0xB/0xC)` + `Broadcast(0x2C)` | | `UIElement_Panel::OpenTab` / `InqTabFromPage` / `ListenToElementMessage` | `0x0046BE20` / `0x0046BEB0` / `0x0046BF90` | switching entry points, `0x18`/`0x19`/`0x29` messages | | `UIElement_ListBox::AddItemFromTemplateList` | `0x0046F2B0` | `0x64` array indexing | | `UIElement_ListBox::AddItem(BaseProperty*)` | `0x0046F170` | `0x63`/`0x62` struct member order | | `UIElement_ListBox::AddItem(did, elementId, parent)` | `0x0046EDC0` | `CreateChildElementByDid` seam | | `UIElement_Scrollable::OnSetAttribute` | `0x00474630` | `0x71` horizontal / `0x72` vertical | | `UIElement_Scrollable::GetScrollbarPointer_` | `0x00473EC0` | `FindRelative` + `DynamicCast(0xB)` validation | | `UIOption_CheckboxBitfield64::CreateChildren` | `0x00485DF0` | rows built from template index 0 | | `UIOption_CheckboxBitfield64::ListenToElementMessage` | `0x00485AE0` | `SetBitsOnOrOff` click math | | `UIOption_CheckboxBitfield64::Refresh` | `0x004859C0` | any-bit-set checked predicate | | `UIOption_CheckboxBitfield64::AddChild` | `0x004862A0` | `ChildInfo{mask,label,tooltip}`, table enum `0x10000006` | | `UIElement::RegisterElementClass(0x10000037/38/36/44, …)` | `0x0048522A` / `0x0048436A` / `0x00484A3A` / `0x00485EDA` | the four UIOption class ids | Fixture decodes were performed with a throwaway JSON reader over the committed `tests/AcDream.App.Tests/UI/Layout/fixtures/*.json` (scratchpad only, not committed). Nothing in the repository was modified except this document.