feat(ui): port retail spellbook interactions

Resolve the authored spell shortcut row prototype so the spellbook presents the retail icon, name, separator, selected overlay, and scrollbar geometry. Port exact school and level filters, stable display ordering, selection exposure, and learned-spell drags into the open favorite bar.

Route deletion through the shared retail confirmation dialog and send CM_Magic::Event_RemoveSpell only after an affirmative answer, leaving the inbound server notice authoritative for list state.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-15 16:26:30 +02:00
parent ad30c37a48
commit ac2ca8f965
17 changed files with 727 additions and 47 deletions

View file

@ -531,7 +531,7 @@ behavior. Estimated 1726 days focused work, 35 weeks calendar.
- **Missile/portal VFX campaign Steps 05 implemented and independently reviewed 2026-07-14.** The retail oracle and packet fixtures are pinned, complete `PhysicsDesc` plus F754/F755 parsing and nine-channel gates are shipped, and App now has one canonical `LiveEntityRuntime` record per server-object incarnation. Logical register/unregister is separate from spatial rebucketing, so landblock churn and attached equipment retain identity without replaying renderer/script creation or reconstructing from stale spawn data. Canonical materialized and visible target/radar views are distinct; pickup/parent leave-world preserves owners while pausing root simulation; spawn publication is once per incarnation. `GpuWorldState` is spatial-only for live objects. Production PhysicsScript and Animation loading shares a narrow `DatCollection`-backed compatibility reader for retail's inherited blocking-particle payload, including mesh-side preloading; the proven concurrent-safe DatReaderWriter 2.1.7 read path avoids blocking update-thread effects behind streaming. Typed-table selection preserves DAT order and retail's first `intensity <= Mod` boundary, and live `PhysicsDesc` effect defaults replace rather than fall back to Setup. The Step 4 scheduler is now one serial FIFO per owner with duplicate stacking, catch-up dispatch, deterministic delayed `CallPES`, complete hook-router fan-out, update-frame clock publication, retail cell/Frozen eligibility, and structural rejection of malformed zero-time recursive DAT chains without rejecting valid timed weather loops. `EntityEffectController` retains pre-create F754/F755 in one mixed per-GUID FIFO, resolves local identity only through `LiveEntityRuntime`, replays once only after the canonical owner is fully ready and in-world, drops later plays while that existing owner is cell-less, routes attached-child updates through the eligible parent, and replaces/clears the live SoundTable on every PhysicsDesc application. Step 5 publishes current rigid root/indexed-part poses after animation and recursively composed held-child transforms, keeping render-only Setup scale out of particle and holding-location anchors; it then drains animation hooks, owner PhysicsScripts, moving emitters/lights, and particle simulation in fixed order. Normal/blocking/anonymous logical emitter identity is exact, including Stop retaining a blocking ID until final-particle retirement; moving and held Setup lights follow their current roots and share retail's PhysicsState Lighting/SetLight transition state; world-released versus parent-local particle behavior is preserved; missing emitter DAT produces an actionable diagnostic and no invented fallback. Drawable meshes no longer collapse stable Setup-part indices; nested attachments update parent-before-child, recursively withdraw on ancestor pose loss, and recover the complete descendant chain only on real publication edges. Loaded↔pending projection edges skip particle update/draw while retaining original absolute creation times and logical identity, and withdraw light presentation; re-entry evaluates elapsed state once without backlog emission or recreating an effect owner. Scripts, particles, lights, translucency, audio, and teardown all share canonical `WorldEntity.Id`; static allocators fail fast before namespace wrap. IA-7, AD-14, TS-10, TS-11, TS-12, TS-13, and AP-67 are retired; AD-32 now covers only the remaining non-effect, non-Parent pre-create packet families, and AD-43 registers the corrupt-DAT zero-time-cycle safety boundary.
- **✓ M2 local attack receive funnel (live-gated 2026-07-15).** Retail `ExecuteAttack` only sends the request; ACE chooses the concrete melee/missile action and returns it in a non-autonomous mt-0 `UpdateMotion`. The local branch now runs that state through the same constructor-defaulted `MoveToInterpretedState` funnel and 15-bit action-stamp gate as remotes, then applies sticky/long-jump tails. The old local-only direct `Commands[]` replay is deleted. Shared conversion lives in `InboundInterpretedMotionFactory`; research: `docs/research/2026-07-11-local-combat-motion-pseudocode.md`.
- **✓ M2 basic retail combat bar (live-gated 2026-07-15).** Production mounts authored `gmCombatUI` LayoutDesc `0x21000073`, shows it only for Melee/Missile, and routes mouse plus keyboard through one `CombatAttackController`. Corrections include the authored wider centered dark-red child range (accepted IA-20), full-width left-to-right bright live attack-charge feedback, exact Speed-left/Power-right justification, silent control-only `AttackDone(ActionCancelled)`, target-frame Keep in View with manual orbit, and persistent corpse motion: the AP-80 velocity-only NPC adaptation can now replace only Ready/Walk/Run, never authoritative Dead/actions. `CombatTargetController` ports the selection-cleared AutoTarget consumer, so a selected creature's authoritative Dead motion clears it and selects the nearest eligible creature when enabled. The 2026-07-12 replacement-corpse correction unifies both multi-frame and static/reactive spawns behind retail's CreateObject lifecycle: apply the wire's Dead state while detached, then `MotionTableManager::HandleEnterWorld` strips Ready→Dead links before the first in-world tick; multiple corpses remaining fallen passed the live user gate that day. Follow-up #205 makes the toolbar read the final canonical selection after a reentrant Auto Target notice and restricts automatic candidates to hostile non-player monsters (intentional PK-edge divergence IA-19); that live gate also passed 2026-07-12. Research: `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`, `docs/research/2026-07-11-combat-target-camera-pseudocode.md`, `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; AP-24/AP-95 retired, AP-80 narrowed, AP-110 narrowed, AP-112 records the remaining attack-start and exact trained-Recklessness seams.
- **M3 cast lifecycle + retained magic UI implemented 2026-07-15; visual gates pending.** `SpellCastingController` ports retail's immediate targeted/untargeted request boundary, component preflight, target eligibility, movement stop, and shared UI-busy lifetime while leaving turn/cast/fizzle/impact outcomes authoritative to ACE. Formula preflight reads decrypted portal.dat formulas, the SCID↔WCID map, canonical account randomization, exact school-focus map, infused-magic properties, and scarab-only substitution. Complete live enchantment packets preserve spell/layer identity, all StatMod fields, client-clock normalization, and exact purge buckets. Authored retained surfaces now include the Magic page of `gmCombatUI`, all eight favorite tabs, equipped caster endowment, Magic-scoped retail keys, `gmSpellbookUI`, `gmSpellComponentUI`, both `gmEffectsUI` windows, and the floating Helpful/Harmful effect indicators. `RetailPanelUiController` ports the shared one-active-panel lifecycle so toolbar and indicator launchers converge on one owner. Exact DAT icons and server-persisted filters/desired components are retained. #L.2/#L.3 close and #L.4/AP-110 narrow. Research: `docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md`.
- **M3 cast lifecycle + retained magic UI implemented 2026-07-15; visual gates pending.** `SpellCastingController` ports retail's immediate targeted/untargeted request boundary, component preflight, target eligibility, movement stop, and shared UI-busy lifetime while leaving turn/cast/fizzle/impact outcomes authoritative to ACE. Formula preflight reads decrypted portal.dat formulas, the SCID↔WCID map, canonical account randomization, exact school-focus map, infused-magic properties, and scarab-only substitution. Complete live enchantment packets preserve spell/layer identity, all StatMod fields, client-clock normalization, and exact purge buckets. Authored retained surfaces now include the Magic page of `gmCombatUI`, all eight favorite tabs, equipped caster endowment, Magic-scoped retail keys, `gmSpellbookUI`, `gmSpellComponentUI`, both `gmEffectsUI` windows, and the floating Helpful/Harmful effect indicators. The spellbook now resolves retail UIItem prototype `0x10000343` for its one-column icon/name/separator rows and selected overlay, binds the authored scrollbar, ports exact filters/display ordering, drags learned spells into the open favorite bar without mutating their source, and reuses `RetailDialogFactory` for server-authoritative spell deletion (`CM_Magic::Event_RemoveSpell`, `0x01A8`). `RetailPanelUiController` ports the shared one-active-panel lifecycle so toolbar and indicator launchers converge on one owner. Exact DAT icons and server-persisted filters/desired components are retained. #L.2/#L.3 close and #L.4/AP-110 narrow. Research: `docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md`.
- **Retail client command families implemented 2026-07-13; corrective live gate pending.** One shared typed catalog now separates retail client actions from ACE administrator commands for both chat backends. Named-decomp ports cover recall/house/PK travel; age/birth; framerate, lock, version, location, corpse, and die confirmation; clear plus named/automatic UI layouts; AFK/consent; emotes; friends; squelch/filter/message types; and fill-components. App owns execution, Core owns authoritative friends/squelch state, Core.Net owns exact UIQueue/ControlQueue packets. Confirmation reuse now ports retail `DialogFactory` contexts, queue groups/priority, fresh DAT roots, property results, callbacks/close notices, and server abort handling; `/die`, gameplay request tuples, and guarded item-use prompts share its type-1 LayoutDesc `0x2100003C` presenter. The first live gate passed the family except for raw suicide-success code `0x004A` and the title-bar-only FPS presentation; the correction maps retail's text and mounts SmartBox element `0x10000047` with live two-decimal `FPS`/`DEG`. #L.6 is closed; TS-31/TS-47 are narrowed. Research: `docs/research/2026-07-13-retail-client-command-families-pseudocode.md`, `docs/research/2026-07-13-retail-dialog-factory-pseudocode.md`.
- **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158.
- **✓ CORRECTIVE PORT — live character experience qualities** (2026-07-13, #217; connected gate passed). Added retail `PrivateUpdatePropertyInt64 (0x02CF)` so Total Experience, the level-progress meter, and Skills' Unassigned Experience refresh from the same authoritative server update; Total XP value alignment now matches the authored right-justified layout.

View file

@ -6,8 +6,9 @@
LANDED 2026-07-15.** The complete connected melee/missile, death/loot,
inventory, and item-giving demo is user-gated. The shared projectile/effect
foundation is hardened through Step 9 and its single-client visual gate passes.
Next is the F.4/L.1d retail cast-lifecycle gap audit, then F.5 spellbook and
active enchantments. Carried: the two-client portal/observer VFX gate,
Next is the connected F.5 spellbook/favorite-bar/delete visual gate, followed
by cast/enchantment presentation and the two-client portal/observer VFX gate.
Carried:
#145-residual, #116 slide-response, R6/TS-42, and Track MP0.
---
@ -649,6 +650,12 @@ The first connected gate exposed and corrected two retained-import defects:
spellbook tabs are authored stateful text controls (not buttons), and retail
text starts with zero margins. Real-DAT fixtures now pin spellbook binding,
Closed/Open child-state propagation, and Magic-tab strings I through VIII.
The follow-up spellbook slice now clones the DAT-authored one-column
spell-shortcut presentation (icon, name, separator, selected overlay), binds
its authored scrollbar, ports exact school/level filter masks and stable
display-order sorting, carries learned spells into the open favorite bar, and
routes Delete through the shared retail confirmation factory plus
server-authoritative `CM_Magic::Event_RemoveSpell (0x01A8)` response path.
**Phases to ship:**
- **F.4** — Spell cast state machine (buffs + recalls first, projectile

View file

@ -240,6 +240,105 @@ 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.
### Spellbook row, selection, filtering, dragging, and deletion
Named-retail anchors:
- `gmSpellbookUI::IsFilteredOut @ 0x0048AEB0`
- `gmSpellbookUI::GetSortedInsertionPlace @ 0x0048B440`
- `gmSpellbookUI::SetSelected @ 0x0048B540`
- `gmSpellbookUI::UpdateFilter @ 0x0048B5E0`
- `gmSpellbookUI::AddSpell @ 0x0048B7A0`
- `gmSpellbookUI::DeleteSpellDialogCallback @ 0x0048BC10`
- `gmSpellbookUI::DeleteSpell @ 0x0048BD50`
- `gmSpellbookUI::ListenToElementMessage @ 0x0048BFD0`
- `UIElement_UIItem::Init_UIItem_Spell_Shortcut @ 0x004E1140`
- `UIElement_ItemList::ItemList_AddSpellShortcut @ 0x004E40A0`
- `UIElement_ItemList::ItemList_InsertSpellShortcut @ 0x004E41B0`
- `CM_Magic::Event_RemoveSpell @ 0x006A3220`
DAT structure:
```text
spell list 0x10000295 (280 x 224)
spell scrollbar 0x10000296 (16 x 224)
spell-row prototype property 0x1000000E -> 0x10000343
shared UIItem catalog 0x21000037
row prototype 0x10000343 (280 x 32)
row background 0x06001396
selected overlay child 0x10000342 -> 0x06001397
label child 0x10000344 (x=42, width=230)
icon child 0x1000033B (x=0, 32 x 32)
delete button 0x100002A5
```
The row's authored background contains the narrow horizontal separator visible
between entries. It is not an invented procedural line. The selection surface
is a separate overlay, so selecting a spell does not replace its icon or label.
```text
AddSpell(spellID):
if spell metadata is missing: return
if IsFilteredOut(spell): return
insertion = first existing row whose displayOrder > spell.displayOrder
list.InsertSpellShortcut(spellID, insertion)
SetSelected(spellID):
for each spell shortcut row:
row.selectedOverlay = (row.spellID == spellID)
if selected: list.ScrollItemIntoView(row)
selectedSpellID = spellID
ListenToElementMessage(message):
if message came from a spell shortcut row:
if message kind is click or double-click: SetSelected(row.spellID)
if message kind is AddSpellShortcut/drag notice:
CM_Magic.SendNotice_AddSpellShortcut(row.spellID)
if message is click on a school/level filter:
UpdateFilter(clickedFilter)
if message is click on 0x100002A5:
DeleteSpell()
UpdateFilter(button):
mask = exact school/level bit assigned to button
if button is currently Highlight: filters |= mask
else: filters &= ~mask
if filters changed:
CM_Character.Event_SpellbookFilterEvent(filters)
rebuild list from authoritative learned spells
scroll list to first row
IsFilteredOut(spell):
school bits: Creature=0x0001, Item=0x0002, Life=0x0004,
War=0x0008, Void=0x2000
level bits: I=0x0010 through VIII=0x0800
return school bit is disabled OR level bit is disabled
DeleteSpell():
if selectedSpellID == 0: return false
name = ClientMagicSystem.GetSpellName(selectedSpellID)
message = "Are you sure you want to remove {name} from your spellbook? "
"You will no longer be able to cast this spell unless you learn it again!"
dialog properties include selectedSpellID under 0x1000003F
DialogFactory.MakeCallbackDialogInCurrentUI(message, callback)
DeleteSpellDialogCallback(dialogProperties):
if confirmation result property 0x92 is true:
spellID = property 0x1000003F
CM_Magic.Event_RemoveSpell(spellID)
CM_Magic.Event_RemoveSpell(spellID):
send GameAction opcode 0x01A8 followed by spellID
RecvNotice_SpellRemoved(spellID):
rebuild from the authoritative player description
```
The client does not optimistically erase the row when Yes is clicked. It waits
for the server's `MagicRemoveSpell` notice. Dragging a row similarly carries a
spell-shortcut identity into the open spellcasting favorite list; it never
removes the spell from the learned-spell set.
## Active enchantment panels
Retail: `gmEffectsUI::PostInit` `0x004B7560` and