feat: port retail magic lifecycle and retained spell UI

Complete the retail cast-intent, target, component, enchantment, and busy-state paths; mount the DAT-authored spell bar, spellbook, component book, effects panels, and shared panel lifecycle; and add scoped input plus conformance coverage.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-15 10:55:22 +02:00
parent 7b7ffcd278
commit 07be994d97
84 changed files with 17822 additions and 1051 deletions

View file

@ -5136,49 +5136,52 @@ responds as if the user clicked the item.
## #L.2 — Spellbook favorites panel
**Status:** OPEN
**Status:** DONE 2026-07-15
**Severity:** MEDIUM
**Filed:** 2026-04-26 (deferred from Phase K)
**Component:** ui / magic
**Description:** In `MagicCombat` scope, 1-9 should fire
`UseSpellSlot_1..9` (distinct from hotbar). Requires a small UI to
pin favorite spells + a spellbook tab nav. Cross-references issue
#L.3 (combat-mode dispatch).
**Resolution:** The authored `gmSpellcastingUI` page inside combat layout
`0x21000073` now exposes all eight server-persisted favorite tabs, equipped
caster endowment, spell selection/name, drag-remove/drop-add favorite edits,
cast button, and `UseSpellSlot_1..9`. Cast intent goes through the single
`SpellCastingController`; ACE remains authoritative for turning, motion,
components, mana, fizzle, impact, and completion.
The same slice also mounts the authored Helpful/Harmful effect indicators
(`0x21000071`, buttons `0x100000F5/F6`) and routes their panel IDs 4/5 through
the shared retail `gmPanelUI` lifecycle, so closed effect lists have an exact
reopen path and replace the current main panel instead of becoming orphaned.
---
## #L.3 — Combat-mode tracking + scope-aware Insert/PgUp/Delete/End/PgDn dispatch
**Status:** OPEN
**Status:** DONE 2026-07-15
**Severity:** MEDIUM
**Filed:** 2026-04-26 (deferred from Phase K)
**Component:** input / combat
**Description:** Insert/PgUp/Delete/End/PgDn mean different things in
melee / missile / magic combat modes (per retail keymap MeleeCombat /
MissileCombat / MagicCombat blocks). Phase K has the bindings and the
scope stack; what's missing: `CombatState.CurrentMode` field +
listener for the server-side `SetCombatMode` packet (likely 0x0053 or
similar — confirm against ACE source). When mode arrives, push the
appropriate scope; when leaving combat, pop.
**Resolution:** Every binding now persists its retail `InputScope`; schema 4
migrates older files. `CombatState.CurrentMode` selects the Melee/Missile/Magic
shadow scope, held actions release atomically on any scope/binding transition,
and reentrant callbacks cannot replay a stale held-chord snapshot. Settings
conflict checks and rebinds preserve the scoped identity.
---
## #L.4 — F-key panels: Allegiance / Fellowship / Skills / Attributes / World / SpellComponents
**Status:** OPEN
**Status:** PARTIAL — magic/character/inventory surfaces shipped
**Severity:** LOW
**Filed:** 2026-04-26 (deferred from Phase K)
**Component:** ui
**Description:** Retail F3-F6, F8-F12 toggle UI panels for various
character data. Phase K has the bindings (`ToggleAllegiancePanel`,
`ToggleFellowshipPanel`, `ToggleSpellbookPanel`,
`ToggleSpellComponentsPanel`, `ToggleAttributesPanel`,
`ToggleSkillsPanel`, `ToggleWorldPanel`, `ToggleInventoryPanel`); the
panels themselves don't exist. Each is its own design feature.
Inventory (F12) is the most-requested.
**Current state:** Retained Inventory, Skills, Attributes, Spellbook, and
SpellComponents panels now exist and their F-key actions route through the
window manager. The spell/component book is the authored `0x21000034` tree,
with learned-spell filters, exact DAT spell/component icons, component
categories, selection, scrolling, and server-persisted desired amounts.
Allegiance, Fellowship, and World remain separate open panel features.
---

View file

@ -128,6 +128,23 @@ window registration, plugin mounts, cursor feedback, layout persistence, and the
retained tick/draw/restore/dispose paths. Panel-specific construction must not
move back into `GameWindow.OnLoad`.
Magic follows the same boundary. Core `Spellbook` is the one learned/favorite/
desired/enchantment state projection; Core.Net owns exact manifest and live
message parsing; App `SpellCastingController` owns validated cast intent; and
the retained spell bar, spell/component book, and effects controllers only
project that state and invoke supplied actions. ACE remains authoritative after
the targeted/untargeted request for turning, animation, mana/components,
fizzle, impact, damage, and completion.
The shared LayoutDesc importer resolves inherited controller trees with retail
`LayoutDesc::InqFullDesc`/`ElementDesc::Incorporate` child identity semantics;
panel controllers never reconstruct missing inherited widgets by hand.
`RetailPanelUiController` is the single retained owner of retail
`gmPanelUI::RecvNotice_SetPanelVisibility` semantics: one active main-panel
child, optional DAT-property-driven deferred restoration, and visibility
reconciliation for persistence. Toolbar buttons and the authored Helpful/
Harmful effects indicators both send panel IDs into this owner; neither owns a
parallel window-lifecycle map.
---
## Project Structure (current + target)

View file

@ -127,7 +127,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| AP-14 | Encumbrance multiplier is a rough piecewise-linear stand-in (1.0→50%, ~0.7@100%, 0.1@300%) for retail's exact curve | `src/AcDream.Core/Items/ItemInstance.cs:187` | Hand-fit segments capture the curve's shape for scaffolding | Client-side burden-scaled effects (speed prediction) differ from retail at most burden ratios when loaded | r06 §6 (retail encumbered multiplier curve) |
| AP-15 | WeenieError translation table covers only ~30 common codes (from ACE enum docs, not retail string_table.bin); unknown codes render raw hex | `src/AcDream.Core/Chat/WeenieErrorMessages.cs:26` | Untranslated codes are rare, fall back losslessly, 30-second add when reported | Server messages outside the table show as raw hex instead of the retail sentence | retail string_table.bin; ACE WeenieError*.cs |
| AP-16 | Point/spot lights selected per-object / per-cell as the **8 nearest reaching lights** (sphere-overlap, nearest-first) via `LightManager.SelectForObject`, capped at `MaxLightsPerObject=8`; called from `WbDrawDispatcher.ComputeEntityLightSet` (objects) and `EnvCellRenderer.GetCellLightSet` (cell shells). Retail's bake (`SetStaticLightingVertexColors`) sums ALL reaching static lights per vertex with no count cap. Retail's *hardware* path (`minimize_object_lighting` 0x0054d480) DOES cap at 8 per object, so the cap is faithful to retail's hardware path — not to its bake path. The `LightManager.Tick` UBO path survives for DIRECTIONAL (sun) lights only; `mesh_modern.vert`'s UBO loop skips point/spot entries (`posAndKind.w != 0 → continue`) — point lights reach the shader exclusively via the per-object SSBO (binding 5) | `src/AcDream.Core/Lighting/LightManager.cs:234` (`SelectForObject`); `MaxLightsPerObject` ~line 174; call sites `WbDrawDispatcher.ComputeEntityLightSet` + `EnvCellRenderer.GetCellLightSet` | Matches retail's hardware constraint (8 lights per object/cell); selection is nearest-sphere-overlap which faithfully allocates lights to the surfaces that actually see them | Surfaces reached by >8 point lights are dimmer than retail's uncapped bake — rare (a dungeon room has a handful of torches), but real; see AP-35 for the bake-vs-GPU-evaluate architecture difference | `minimize_object_lighting` 0x0054d480 (retail's 8-light hardware cap); `SetStaticLightingVertexColors` 0x0059cfe0 (retail's bake, no count cap) |
| AP-17 | Spell metadata from third-party CSV (3,956 rows, bad rows silently skipped), not the portal.dat SpellTable; Family feeds stacking decisions | `src/AcDream.Core/Spells/SpellTable.cs:10` | The dat spell-table port (obfuscated/encrypted aspects) wasn't done; CSV closed #11 fast and unblocked #6 stacking | Any CSV↔dat drift (wrong Family, missing rows) silently produces wrong buff-stacking winners and wrong panel info | portal.dat SpellTable 0x0E00000E |
| AP-17 | Descriptive spell metadata and Family still come from a third-party CSV (3,956 rows, bad rows silently skipped); retail DAT now supplies decrypted formulas, school, exact icons, level filters, and component preflight | `src/AcDream.Core/Spells/SpellTable.cs:10`; `src/AcDream.App/Spells/MagicRuntime.cs` | CSV originally closed #11 and unblocked #6 stacking; M3 moved every cast-critical formula/icon/level decision to portal.dat but has not yet replaced descriptive/Family metadata | CSV↔DAT drift can still produce wrong stacking winners or panel name/description/beneficial classification; component choice and level/icon presentation no longer drift here | portal.dat SpellTable 0x0E00000E |
| ~~AP-18~~ | **RETIRED 2026-07-10 — faithful retail radar port.** Exact `RGBAColor_Radar*` floats were recovered from named static data and `gmRadarUI::GetBlipColor` was re-ported with `_blipColor` overrides plus portal/vendor/attackable-creature/admin/PK/PKLite/free-PK/fellowship precedence. The old implementation was not merely hue-tuned: it also had wrong portal/vendor colors and an incomplete dispatch matrix. | `src/AcDream.Core/Ui/RadarBlipColors.cs` + radar classification tests | — | — | `gmRadarUI::GetBlipColor` 0x004d76f0; static RGBA initializers at named decomp pc:1089736-1089804 |
| AP-19 | `PortalSideEpsilon` 0.01 (≈1 cm) instead of retail F_EPSILON ≈ 0.0002 — a documented render-root-lag tolerance, NOT a retail constant. DO-NOT-RETRY: T2 (BR-4) tried the retail value; CornerFloodReplay refuted it | `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs:49` | Retail's tight epsilon only works with eye-exact swept curr_cell tracking; our viewer cell lags the eye by up to ~1 cm at pressed corners. Tighten after the #108-membership family + cdstW near-clip pin land | A 1 cm misclassification band at portal planes can flood or cull a portal the eye hasn't crossed — one-frame leaks / grey flashes at knife-edge doorway/corner positions | F_EPSILON @0x007c8c70; `PView::InitCell` 0x005a4b70 |
| AP-20 | Sub-pixel view-polygon vertex merge fixed at 1080p-reference NDC units (2/1080); retail merges at ~1 actual screen pixel | `src/AcDream.App/Rendering/PortalProjection.cs:179` | Unit approximation whose coarseness only strengthens convergence — the merge is the flood's fixpoint floor (replaced MaxReprocessPerCell=16) | At 4K+ a legitimately visible 12 px sliver aperture collapses to degenerate and rejects — a thin/distant doorway stops admitting its flood slightly earlier than retail | `Render::copy_view` 0x0054dfc0 |
@ -210,7 +210,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| ~~AP-107~~ | **RETIRED 2026-07-11 (Wave 3.3 / #197)** — typed `OfferPrimaryClick` returns `NotActive`, `ConsumedSuccess`, or `ConsumedRejected`; every retained item surface plus radar/world offers active target mode before local selection/open/use fallback. Rejections are consumed and cannot drift selection. | `src/AcDream.App/UI/ItemInteractionController.cs`; inventory/paperdoll/toolbar/radar/world call sites | — | — | `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0` |
| AP-108 | Paperdoll/AutoWield still omit full `AutoWieldIsLegal`/dual-wield rules, double-click examine/drag from the doll, body-part selection lighting, and retail's synchronous `" - cannot unwield the %s"` failure suffix (the current send seam reports rejection asynchronously). **Primary replacement retired from this row 2026-07-14:** inventory activation and paperdoll drops share the confirmed blocker transaction, preserve explicit slot intent, relocate an already-worn item to an explicitly selected compatible slot, and emit retail's successful move-to-backpack status. **Aetheria retired 2026-07-13.** | `src/AcDream.App/UI/Layout/PaperdollController.cs`; `src/AcDream.App/UI/AutoWieldController.cs` | Basic equip slots, Aetheria, and live doll work; primary weapon, incompatible shield, and mismatched ammo blockers sequence through server-confirmed dequip→wield in both peace and war | Remaining illegal/off-hand cases, asynchronous dequip rejection wording, doll examine/drag, and selection lighting still differ functionally | `CPlayerSystem::AutoWield @ 0x00560A60`; `ACCWeenieObject::BlocksUseOfShield @ 0x0055D3E0`; `gmPaperDollUI @ 0x004A3590..0x004A5F90` |
| AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` |
| AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, spellbook/effects/favorite spell bars, residual social/examine/floating chat, quests/map/vitae/options/smartbox, vendor/trade/salvage/tinkering, and D.6 nameplates/floaters | D.5/D.6 roadmap; retained layout registration set | Basic `gmCombatUI` now covers the active M2 melee/missile loop; Wave 10 lands each remaining surface against authoritative state | Large portions of retail gameplay still have no production UI | Named `gm*UI::PostInit` methods and LayoutDesc catalog |
| AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, residual social/examine/floating chat, quests/map/vitae/options/smartbox, vendor/trade/salvage/tinkering, and D.6 nameplates/floaters | D.5/D.6 roadmap; retained layout registration set | Basic combat plus M3 spellbook/component/effects/favorite-spell surfaces now cover the active melee/missile/magic loops; later waves land each remaining surface against authoritative state | Large portions of retail gameplay still have no production UI outside the current combat/magic/inventory/character loops | Named `gm*UI::PostInit` methods and LayoutDesc catalog |
| ~~AP-111~~ | **RETIRED 2026-07-11 (M2 held-object parenting)** — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live `0xF749` ParentEvent is parsed with retail sequence freshness; a focused render controller resolves `Setup.HoldingLocations`, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | `src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs`; `src/AcDream.Core/Meshing/EquippedChildAttachment.cs`; `src/AcDream.App/Rendering/EquippedChildRenderController.cs` | — | — | `ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310`; `SmartBox::HandleParentEvent @ 0x004535D0`; `CPhysicsObj::set_parent @ 0x00515A90`; `CPhysicsObj::UpdateChild @ 0x00512D50` |
| AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, ready-stance gating, request/release, `MaybeStopCompletely`, server-response queueing, and auto-repeat, but still omits `StartAttackRequest`'s `FinishJump` call and exact trained-Recklessness visibility semantics (IA-20 keeps the dark range as the accepted baseline) | `src/AcDream.App/Combat/CombatAttackController.cs`; `src/AcDream.App/UI/Layout/CombatUiController.cs` | The shared player movement owner now performs retail's server-control-gated full stop and movement report before an attack build; the remaining seams require the jump owner and a distinct Recklessness treatment | Starting an attack while charging a jump may not finish that jump exactly when retail does; trained/untrained Recklessness presentation is identical | `ClientCombatSystem::StartAttackRequest @ 0x0056C040`; `CommandInterpreter::MaybeStopCompletely @ 0x006B3B90`; `gmCombatUI::ListenToElementMessage @ 0x004CC430` |
| AP-113 | Invalid lifestone-command arguments display the local text `Usage: /lifestone`; retail definitely emits a local usage/error line but Binary Ninja misidentifies the referenced wide-string address, so its exact wording is not yet recovered | `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`; `RetailClientCommandCatalog.cs` | The behavior boundary is exact (handled locally, no chat and no game action); only a low-impact diagnostic sentence differs | `/ls now` can show different wording/color from retail while still refusing the invalid request correctly | `ClientCommunicationSystem::DoLifestone @ 0x0056FC70` |

View file

@ -1,6 +1,6 @@
# acdream — strategic roadmap
**Status:** Living document. Updated 2026-07-15. **M3 active.** M2's connected demo loop is complete: melee and missile weapon switching, target-facing, combat animation and damage/death presentation, loot/inventory, stack operations, and giving an item to a world target have all passed user gates. The projectile/PhysicsScript/particle/portal foundation is implemented through its Step 9 automated hardening gate; single-client missile and protection visuals pass, while the final two-client portal-out/materialization observer gate remains open. The immediate work order is an F.4/L.1d retail-conformance audit against that shipped foundation, implementation of the remaining cast lifecycle, then F.5 spellbook and active-enchantment UI.
**Status:** Living document. Updated 2026-07-15. **M3 active; automated implementation complete, visual gates pending.** M2's connected demo loop is complete. M3 now has the retail cast-intent/component/target lifecycle, exact live enchantment wire state, scoped Magic input, retained spell bar, spellbook, component book, positive/negative effects panels, authored effect-indicator buttons, and shared `gmPanelUI` switching over the Step 9 projectile/PhysicsScript/particle/portal foundation. The remaining M3 work is the connected single-client magic-UI/casting gate and final two-client portal-out/materialization observer gate.
**Purpose:** One source of truth for where the project is and where it's going. Every observed defect or missing feature has a named phase that owns it; when something looks wrong in-game, look here to find the phase that'll address it. Implementation details live in per-phase specs under `docs/superpowers/specs/`, not in this file.
---
@ -11,17 +11,19 @@
turn/windup/release/projectile/impact lifecycle, self-cast a buff and see its
active enchantment, then recall through the DAT-driven portal presentation.
**Next concrete work, in order:**
**Current gate order:**
1. Reconcile current E.5 wire, M2/M3 projectile/VFX, motion, and enchantment
state against named-retail F.4/L.1d; record only the proven remaining gaps.
2. Port and conformance-test those cast-state and animation lifecycle gaps,
including facing, release, interruption/fizzle, recoil, and authoritative
completion.
3. Ship F.5's retained spellbook and active-enchantment surfaces over the
existing shared state/command seam.
4. Run the pending two-client portal-out/materialization observer visual gate
before M3 closes.
1. **Done (automated):** named-retail F.4/L.1d reconciliation, full live
enchantment messages, account/focus-aware component formula preflight,
retail target eligibility, authoritative cast request/completion ownership.
2. **Done (automated):** authored Magic combat page, eight favorite tabs,
equipped caster endowment, scoped retail keys, spellbook/component book,
positive/negative active-enchantment panels, Helpful/Harmful indicator
buttons, and the retail one-active-main-panel lifecycle.
3. **Pending user visual gate:** connected projectile/self-buff casts plus the
complete retained magic UI interaction checklist.
4. **Pending user visual gate:** two-client portal-out/materialization observer
presentation before M3 closes.
Opportunistic UI/physics fixes are no longer the work-order driver. They enter
the active slice only when they block this demo or represent a severe regression.
@ -527,13 +529,14 @@ 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`.
- **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.
- **✓ SHIPPED — D.5.4 — Client object/item data model (foundation).** Shipped 2026-06-18 (`b506f53`..`a33e897`, 11 commits). Renamed `ItemRepository``ClientObjectTable` / `ItemInstance``ClientObject`; broadened the table to hold EVERY server object (retail `weenie_object_table` shape). `CreateObject` is now the canonical merge-upsert (`ClientObjectTable.Ingest`, retail `SetWeenieDesc` semantics) via a new Core.Net `ObjectTableWiring` (off GameWindow); `DeleteObject` evicts; `PlayerDescription` is a membership manifest (`RecordMembership`); live container-membership index (`GetContents`, retail `object_inventory_table`). `_liveEntityInfoByGuid` retired (selection/describe resolve from the one table). Root fix: the old enrich-existing-only `EnrichItem` dropped `CreateObject`s for items with no `PlayerDescription` stub — live-Coldeve 4/6 hotbar slots blank; items are now created, not dropped. **Crux resolved:** retail is TWO tables (`object_table` + `weenie_object_table`), NOT one — acdream's `WorldEntity` (3D system) + `ClientObjectTable` (data/UI) split was already architecturally faithful; the fix was the ingestion path, not a table unification. 2671 tests green.
- **Roadmap correction (2026-07-10):** the completion order is now the architecture-first campaign in `docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md`. Retail `gmToolbarUI` is object-only: preserve `ShortCutData.index_`, `objectID_`, and `spellID_`, but do not invent spell glyphs on this bar. `PlayerModule::favorite_spells_[8]` feeds separate spell bars.
- **✓ D.5.3 — Toolbar selected-object display (issue #141) SHIPPED.** Exact health/player/pet/Free-PK policy, selected-item mana (`0x100001A2`), stack entry (`0x100001A3`), stack slider (`0x100001A4`), formatted count/name, shared split quantity, click-to-use, peace/war, health/name, and paperdoll self/upper-item selection all passed live by 2026-07-11. Exact raw `ShortCutData` preservation is already shipped in Wave 4.2; the eight `favorite_spells_` lists belong to the separate spellbook/spell-bar phase.
- **☐ D.5.5+ — Core panels.** Inventory (`gmInventoryUI`/`gmBackpackUI`), equipment/paperdoll (`gmPaperDollUI`/`gm3DItemsUI` + the `UiViewport` 3D doll), vendor, trade, spellbook. Research drop done (`docs/research/2026-06-16-*`). Depends on **D.5.4** (data model) + the item-slot/list/icon spine (D.5.1/D.5.2) + the **window manager** (Plan 2: open/close/z-order/persist + faithful grip/dragbar drag-resize) + the drag-drop spine wired (`UiRoot` has the chain; the per-cell accept/drop hooks are still stubs in `UiField`). Also deferred from D.5.1: drag/reorder + the `AddShortcut`/`RemoveShortcut` mutate wire.
- **☐ D.5.5+ — Remaining core panels.** Inventory, equipment/paperdoll, spellbook/component book, and active-effects surfaces are shipped through the retained window manager. Vendor, trade, salvage, tinkering, and the remaining social/world panels stay open. Research drop: `docs/research/2026-06-16-*`; current magic research: `docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md`.
- **D.6 — HUD.** **Radar/compass IMPLEMENTED 2026-07-10; jump power bar IMPLEMENTED 2026-07-13; live-world visual gates pending.** The radar is retained retail `gmRadarUI` (`LayoutDesc 0x21000074`): a 120×140 circular face (`0x06004CC1`) with orbiting N/E/S/W tokens, a fixed bright-green player marker, and coordinate footer (`0x06004CC0`). Projection is `radarPixelRadius / radarRange` with 75 m outdoor / 25 m indoor ranges — no 1.18 factor or scrolling strip. The separate `gmFloatyPowerBarUI` (`LayoutDesc 0x21000072`) now appears only while jump is pending and clips the authored JumpMode fill left-to-right from the movement controller's shared 01 charge. Both live in `AcDream.App/UI` behind focused controllers; `GameWindow` supplies state delegates only. Remaining D.6: target name plate, damage floaters, and other world-space HUD elements through the raw `TextRenderer` path. See slice 06 §A.2, `docs/research/2026-07-10-retail-radar-pseudocode.md`, `docs/research/2026-07-13-retail-jump-powerbar-pseudocode.md`, and the named `gmRadarUI`/`gmPowerbarUI` decomp.
- **✓ SHIPPED — D.7 — Cursor manager (user-confirmed 2026-07-11).** All reachable `ClientUISystem::UpdateCursorState @ 0x00564630` branches resolve through production DAT EnumIDMap table 6, with event-ordered global-default versus captured/hovered `MediaDescCursor` layering from `UIElementManager::CheckCursor @ 0x0045ABF0`. OS fallback remains only AP-72 and emits a visible diagnostic. The live gate covered world/object, combat, targeted-use, text, move/resize, and drag contexts; combat-indicator mouse clicks and the quick key share the same toggle command.
- ~~**D.8 — Sound.**~~ **Superseded — shipped as Phase E.2** (`SoundTable`/`Sound` dat decode, OpenAL 16-voice engine, per-entity 3D positional audio via `AudioHookSink`). Entry kept here for history; see the shipped table.

View file

@ -634,6 +634,18 @@ protection effects pass the single-client visual gate. M3 begins with a focused
named-retail gap audit of F.4/L.1d—not a rewrite of this foundation—then fills
the spellbook/active-enchantment UI and completes the two-client portal gate.
**Implementation update (2026-07-15):** The automated F.4/L.1d audit and port
are complete. Cast requests now use retail component-formula selection,
target compatibility, shared busy/completion ownership, and exact
targeted/untargeted messages. Live enchantment add/remove/dispel/purge state is
fully parsed with layer identity and client-clock normalization. The authored
Magic combat page, eight favorite tabs, caster endowment, spellbook/component
book, scoped keys, positive/negative effects windows, and the authored
Helpful/Harmful indicators are mounted in the retained runtime. Main-panel
visibility now flows through retail's one-active `gmPanelUI` owner. M3 remains
active only for the connected single-client magic
UI/casting visual gate and the final two-client portal observer gate.
**Phases to ship:**
- **F.4** — Spell cast state machine (buffs + recalls first, projectile
spells second).

View file

@ -0,0 +1,277 @@
# Retail magic UI and casting pseudocode
This note is the implementation oracle for the M3 cast lifecycle and retained
spell UI. Retail addresses refer to the September 2013 EoR named client.
Wire layouts were cross-checked against ACE and the existing holtburger-derived
`PlayerDescriptionParser`.
## Cast request ownership
Retail: `ClientMagicSystem::CastSpell` `0x00568040` and
`ClientMagicSystem::FreeHandsAndCastSpell` `0x00566EF0`.
```text
CastSpell(spellId, useSelectedTarget):
spell = SpellTable[spellId]
if components are required:
formula = GetAppropriateSpellFormula(spell)
if any required component is absent:
display "You do not have all of this spell's components."
return
if spell targets self:
target = local player id (or untargeted when no SmartBox exists)
FreeHandsAndCastSpell(spellId, target)
return
if spell has no target type:
MaybeStopCompletely()
send CastUntargetedSpell(spellId)
increment UI busy count
return
target = selected object
if target == 0:
display "You must select a suitable target."
return
if the selected GUID no longer resolves to an object:
return silently
if ObjectCompatibleWithSpell(target, spellId):
FreeHandsAndCastSpell(spellId, target)
reselect target
FreeHandsAndCastSpell(spellId, target):
MaybeStopCompletely()
if target == 0: send CastUntargetedSpell(spellId)
else: send CastTargetedSpell(target, spellId)
increment UI busy count
```
The request is sent immediately. ACE remains authoritative for turning,
casting motion, component consumption, mana use, fizzle, impact, damage, and
enchantment state. The client does not invent a parallel gameplay outcome.
### Formula selection and target compatibility
Retail: `ClientMagicSystem::GetAppropriateSpellFormula` `0x00567D50`,
`CSpellBase::InqScarabOnlyFormula` `0x00597050`,
`ACCWeenieObject::MagicPackIsOwned` `0x0058CB80`, and
`ClientMagicSystem::ObjectCompatibleWithSpellTargetType` `0x00567230`.
```text
GetAppropriateSpellFormula(spell):
schoolFocusWcid = SchoolOfMagic2WCID(spell.school)
infusedProperty = [War 0x129, Life 0x128, Item 0x127,
Creature 0x126, Void 0x148][school]
if infusedProperty > 0 OR a directly carried side-pack has schoolFocusWcid:
retain every power component in formula order
strongest = highest retained component power
append prismatic taper SCID 0xBC:
power 1 => 1; power 2 => 2; power 3/4/7 => 3;
power 5/6/8/9/10 => 4
return result
return account-customized formula
ObjectCompatibleWithSpellTargetType(target, targetMask):
special = targetMask & 0x8107
reject self when special == 0
reject stacks
require (targetMask & target.type) != 0 OR special != 0
require target.IsPlayer OR target.publicBitfield has BF_ATTACKABLE
require target.petOwner == 0
```
The final Player/Attackable and pet gates apply after both the ordinary type
match and the special-mask bypass. Formula entries are SCIDs; inventory and
favorite-component events use WCIDs, resolved through DualEnumIDMap
`0x27000002`. The magic-school focus map is retail enum category `0x28`, key
`0x10000001`.
## Authoritative enchantment events
Retail dispatchers:
- `CM_Magic::DispatchUI_UpdateEnchantment` `0x006A32F0`
- `CM_Magic::DispatchUI_RemoveEnchantment` `0x006A2FB0`
- `CM_Magic::DispatchUI_DispelEnchantment` `0x006A2F20`
```text
0x02C2 UpdateEnchantment:
unpack one Enchantment record
0x02C4 UpdateMultipleEnchantments:
u32 count
repeat count times: unpack Enchantment record
Enchantment record (60 or 64 bytes):
u16 spellId
u16 layer
u16 spellCategory
u16 hasSpellSetId
u32 powerLevel
f64 startTime
f64 duration
u32 casterGuid
f32 degradeModifier
f32 degradeLimit
f64 lastDegraded
u32 statModType
u32 statModKey
f32 statModValue
if hasSpellSetId: u32 spellSetId
0x02C3 RemoveEnchantment:
u16 spellId
u16 layer
0x02C7 DispelEnchantment:
u16 spellId
u16 layer
0x02C5 / 0x02C8 Remove/DispelMultipleEnchantments:
u32 count
repeat count times: u16 spellId, u16 layer
0x02C6 PurgeEnchantments:
no payload; clear the authoritative active set
```
## Magic combat page (spell bar)
Retail: `gmSpellcastingUI::PostInit` `0x004C5D00`,
`SpellCastSubMenu::Init` `0x004C5A90`, and `gmSpellcastingUI::Cast`
`0x004C6050`.
The spell bar is the authored Magic page inside combat layout `0x21000073`.
It is not an advanced-combat placeholder.
```text
combat basic page 0x1000005C
spellcasting page 0x10000061
spellcasting content 0x100000A2
spell name text 0x1000048B
spellcasting background 0x100000A0
cast button 0x100000B2
endowment icon 0x100000B1
favorite tab buttons 0x100000A3..0x100000A9, 0x100005C2
favorite list groups 0x100000AA..0x100000B0, 0x100005C3
item list within each group 0x100000B6
```
There are eight favorite tabs. Each list is authoritative character state.
Selecting a spell updates its name and current spell. Cast invokes
`ClientMagicSystem::CastSpell(selectedSpell, true)`. PageUp/PageDown and the
retail spell input actions change tab/selection; the cast-current action casts
the current selection. Favorite edits are sent to ACE.
Outbound character events (`CM_Character`):
```text
0x01E3 AddSpellFavorite(spellId:u32, position:i32, tab:i32)
0x01E4 RemoveSpellFavorite(spellId:u32, tab:i32)
0x0286 SpellbookFilterEvent(filter:u32)
0x0224 SetDesiredComponentLevel(componentDid:u32, amount:i32)
```
## Spellbook and component book
Retail: `gmSpellbookUI::PostInit` `0x0048B2B0` and
`gmSpellComponentUI::PostInit` `0x0048A190`.
Both pages are authored in layout `0x21000034` (300 by 600 pixels):
```text
window root 0x100002A8
spellbook tab 0x100002A9
component tab 0x100002AA
close button 0x100002AB
spellbook page 0x100002AC (base 0x10000294 / 0x21000032)
component page 0x100002AD (base 0x100002A7 / 0x21000033)
spell list 0x10000295
school buttons 0x10000298..0x1000029B, 0x100005C0
level buttons 1..7 0x1000029C..0x100002A2
level button 8 0x1000054E
component item list 0x10000464
component scrollbar 0x10000465
```
Spellbook rows come only from the authoritative learned-spell set. Retail
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.
## Active enchantment panels
Retail: `gmEffectsUI::PostInit` `0x004B7560` and
`gmEffectsUI::RebuildList` `0x004B8350`.
```text
layout 0x2100001B
positive root 0x1000011F (inherits 0x10000122)
negative root 0x10000121 (inherits 0x10000122)
shared effects template 0x10000122
effect list 0x10000123
information text 0x10000126
effect UI type property 0x1000000C
```
The `0x10000184`/`0x10000185` values used by `gmPanelUI` are panel-slot
identities, not roots in effects LayoutDesc `0x2100001B`. The two authored
effects roots carry their title/close overrides and inherit the list,
scrollbar, information area, and remaining chrome from `0x10000122`.
Retail `gmPanelUI::SetupChildren` (`0x004BC9E0`) registers those children as
panel IDs 4 and 5. `RecvNotice_SetPanelVisibility` (`0x004BC6F0`) owns one
active child: showing either effect panel hides the previous main panel, while
closing the active child hides the shared host. DAT bool property `0x10000049`
selects the exceptional save-and-restore-previous behavior. The resolved
Helpful/positive root sets it, so closing Helpful restores the preceding
ordinary panel. Harmful/negative does not set it and leaves the host empty.
The reopen path is not the main toolbar. Retail
`gmUIElement_EffectsIndicator::PostInit` (`0x004E6930`) reads effect type 1 or
2 from property `0x1000000C`, and `Update` (`0x004E6A50`) ghosts the button
when no matching enchantment exists. The floating indicators layout is
`0x21000071`; Helpful button `0x100000F5` opens panel 4 and Harmful button
`0x100000F6` opens panel 5. The docked equivalent is layout `0x21000015`.
Retail `LayoutDesc::InqFullDesc` (`0x0069A520`) resolves the base first and
then calls `ElementDesc::Incorporate` (`0x0069B5A0`). Child tables are merged
by element identity: base-only children remain, same-identity children are
incorporated recursively, and derived-only children are appended with their
read order shifted by the retained base-only count.
When visible, each panel rebuilds from the authoritative active-enchantment
snapshot and refreshes each token's duration at most once per second.
`CEnchantmentRegistry::GetEnchantmentsInEffect` (`0x00594540`) merges only the
multiplicative and additive registries, so Vitae and cooldown records neither
appear in these panels nor enable an indicator. Each incoming entry is dueled
against the current winner for its spell category (`Duel @ 0x005942B0`): higher
power wins, with later start time breaking an equal-power tie. Positive and
negative panels then test the winning spell's Beneficial flag and sort
alphabetically.
Live `AddEnchantmentToList @ 0x00593DF0` inserts a new identity at the bucket
head, while an existing identity refreshes in place; bulk UnPack retains its
received head-to-tail order. Since an exact power/start tie keeps the first
duel incumbent, the newest incrementally added identity wins an exact tie.
The Helpful/Harmful indicators intentionally use a different projection.
`CountSpellsInList @ 0x00593CD0` feeds the registry's two indicator counters by
visiting every raw mult/add node before any category duel. Therefore opposite-
sign spells in the same category can enable both indicators while only the duel
winner appears in the effects list.
Remaining duration is `(startTime + duration) - current game time`. Selecting
an active token stores its spell ID, so every layer of that spell highlights
together and shows the shared name/description; clicking either layer again
clears the selection.
## Portal presentation boundary
Recall and portal presentation remains DAT-driven. `/ls` starts the retail
recall motion (`0x10000153`); its animation hooks/PES chain supplies the cloud.
Remote disappearance and materialization use authoritative Hidden/UnHide state
and typed physics scripts. No generic teleport effect, guessed color, or local
teleport outcome is added by the spell UI work.