feat(ui): port retail component book rows

Instantiate gmSpellComponentUI category and component entries from LayoutDesc 0x21000033 instead of synthesizing catalog rows. Preserve localized category titles, authored textures and geometry, live icon/count/desired bindings, selected-object synchronization, and retail's inclusive 0..5000 desired-level contract.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-15 17:38:24 +02:00
parent 15bb61e05d
commit a1175e6aac
13 changed files with 2395 additions and 60 deletions

View file

@ -292,6 +292,102 @@ sorts by `CSpellBase::_display_order`; school and level buttons apply the
server-persisted filter bitfield. Component rows use the installed component
table and the server-persisted desired purchase amounts.
### Component-book listbox templates and synchronization
Named-retail anchors:
- `gmSpellComponentUI::PostInit @ 0x0048A190`
- `gmSpellComponentUI::ListenToElementMessage @ 0x0048A420`
- `gmSpellComponentUI::SyncComponentRegionWithData @ 0x0048A620`
- `gmSpellComponentUI::UpdateComponents @ 0x0048A910`
- `gmSpellComponentUI::RecvNotice_SelectionChanged @ 0x00489C40`
- `gmSpellComponentUI::UpdateBuyRates @ 0x00489FE0`
The component page is not a generic icon catalog. LayoutDesc `0x21000033`
authors a listbox with two entry templates, referenced in order by list
property `0x64`:
```text
component list 0x10000464 (284 x 600)
component scrollbar 0x10000465 (16 x 600)
template 0 / category 0x10000466 (284 x 32, UIElement_Text)
background 0x06001392
centered category font 0x40000004
template 1 / component 0x10000467 (284 x 32, UIElement_Field)
normal background 0x06005E21
Highlight background 0x06005E22
icon region 0x10000468 (x=0, 32 x 32)
name 0x10000469 (x=42, width=180, height=32)
owned count 0x1000046A (x=224, y=0, width=60, height=15)
desired purchase count 0x1000046B (x=224, y=15, width=60, height=15)
```
The seven localized headings are string hashes in local string table
`0x23000001`: `SCARABS`, `HERBS`, `POWDERED GEMS`, `ALCHEMICAL SUBSTANCES`,
`TALISMANS`, `TAPERS`, and `PEAS`. They correspond in order to retail
`SpellComponentCategory` values 0 through 6. Category 6 is Peas; treating it
as an invented "Prismatic Components" group is not retail behavior.
```text
PostInit():
componentList = GetChildRecursive(0x10000464) as ListBox
register component/player/selection notices
if PlayerDescription already arrived: UpdateBuyRates(all components)
UpdateComponents(change):
tracker = PlayerSystem.ComponentTracker
insertion = 0
for category in 0..6:
if tracker.componentLists[category] is empty:
remove a stale category entry for this category, if present
continue
if entry[insertion] is not this category header:
header = list.AddItemFromTemplateList(templateIndex=0, insertion)
header.attribute[0x1000004D] = category
header.text = localizedCategoryTitle[category]
insertion++
reconcile component rows against tracker.componentLists[category]
each new row = list.AddItemFromTemplateList(templateIndex=1, insertion)
row.attribute[0x1000004C] = componentWCID
row[0x10000468] = MakeIcon(componentWCID)
row[0x10000469] = componentName
row[0x1000046A] = totalOwnedCount
row[0x1000046B] = desiredPurchaseCount
row[0x1000046B].inputFilter = NumberInputFilter
insertion++
ListenToElementMessage(message):
if selection changed:
if selected entry is a category:
clear selected world object
else if selected entry is a component:
select the first owned object represented by that component region
if edit completed on 0x1000046B:
parse integer
if 0 <= value <= 5000:
send SetDesiredComponentLevel(componentWCID, value)
update local PlayerModule projection
else:
restore the authoritative desired value in the field
RecvNotice_SelectionChanged():
if the selected world object belongs to a tracked component region:
select and expose its matching component row
else:
clear component-list selection
```
The retained port instantiates these resolved templates through
`ComponentBookTemplateFactory` and `UiTemplateListSlot`, the architectural
equivalent of `AddItemFromTemplateList`. Controllers bind only the live icon,
texts, selection, and edit callbacks. They do not duplicate the row sprites,
fonts, or geometry.
### Spellbook row, selection, filtering, dragging, and deletion
Named-retail anchors: