From 6770381fc3fe2f1a518a74b1093094771fa9300e Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 17 Jun 2026 17:24:03 +0200 Subject: [PATCH] docs(D.5.2): stateful icon-system handoff + roadmap (D.5.1 shipped, D.5.2/D.5.3 next) Extensive handoff for the next session to build the full stateful item-icon system (the 5-layer IconData::RenderIcons composite + effect layer 0x10000005 + overlay ReplaceColor tint + appraise-driven enrichment/re-composition). D.5.1 toolbar flipped to SHIPPED; D.5.2 (icon system) + D.5.3 (toolbar interactivity / selected-object display) registered as next. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/plans/2026-04-11-roadmap.md | 5 +- ...2026-06-17-stateful-icon-system-handoff.md | 127 ++++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 docs/research/2026-06-17-stateful-icon-system-handoff.md diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 549067e9..b7fe34f2 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -431,7 +431,10 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar. - **D.3 — AcFont from portal.dat.** Replace stb_truetype system font with retail `Font` DBObjs (`0x40000000..0x40000FFF`) baked from `RenderSurface` source sheets — see research slice 03 §4. Preserves retail visual identity. **(D.2b dependency — needs the custom renderer.)** - **✓ SHIPPED — D.4 — Dat sprites + 9-slice panel backgrounds.** Shipped 2026-06-14 with D.2b. `TextureCache.GetOrUploadRenderSurface(id, out w, out h)` decodes `RenderSurface` (`0x06xxxxxx`) **directly** (not via the Surface→SurfaceTexture chain — the prove-out finding) → GL `Texture2D`; `DrawSprite` (explicit UV-rect, per-texture batch) added to `TextRenderer` + `UiRenderContext` + a `uUseTexture=2` RGBA frag branch; `UiNineSlicePanel` composes the universal 8-piece bevel (corners `0x060074C3..C6`, edges `0x060074BF/C0/C1/C2`, center `0x06004CC2`). Remaining art polish: the glassy gradient bar fill sprite (D.2b polish). - **D.5 — Core panels.** Attributes (`chunk_00470000.c:FUN_0047ba70`), Skills (same), Paperdoll (`chunk_004A0000.c:FUN_004A5200`), Inventory, Spellbook (`chunk_004C0000.c`), Fellowship, Allegiance. Each uses the port sketches in slice 05. **(Targets `AcDream.UI.Abstractions` — ships with D.2a using ImGui-rendered widgets; reskinned by D.2b.)** The *chat* panel originally listed under D.5 shipped early in Phase I (I.4 input + I.7 combat translator superseded the chat-panel design here); this entry now covers Attributes / Skills / Paperdoll / Inventory / Spellbook / Fellowship / Allegiance only. -- **D.5.1 — Toolbar (action bar) [IN PROGRESS].** First D.5 sub-phase. `gmToolbarUI` (`LayoutDesc 0x21000016`) as the first data-driven game panel: 18 shortcut slots from the persisted `PlayerDescription` SHORTCUT block, real composited icons, click-to-use. New shared widgets `UiItemSlot` (`UIElement_UIItem` 0x10000032, procedural) + `UiItemList` (`UIElement_ItemList` 0x10000031, factory-registered) + `IconComposer` (CPU 5-layer composite, `IconData::RenderIcons` @407524) + the `CreateObject`→`ItemRepository` IconId wiring. Spec/plan: `docs/superpowers/{specs,plans}/2026-06-16-d2b-toolbar-phase1*.md`. Deferred to later D.5 sub-phases: drag/reorder, the AddShortcut/RemoveShortcut mutate wire, meters/slider, spell shortcuts, faithful window manager, inventory, paperdoll. +- **✓ SHIPPED — D.5.1 — Toolbar (action bar).** Shipped 2026-06-16/17 (`30b28c2`→`0e7a083`, branch claude/hopeful-maxwell-214a12). First data-driven *game* panel: `gmToolbarUI` (`LayoutDesc 0x21000016`) — 18 shortcut slots from the persisted `PlayerDescription` SHORTCUT block, real **composited** item icons (opaque type-default underlay via the `EnumIDMap 0x10000004` resolve), **occupancy-gated slot numbers 1–9** (occupied = dark-box peace/war `0x10000042/43`; empty = background `0x1000005e` from cell composite `0x10000341`), **click-to-use** (`ItemHolder::UseObject` → `0x0036`), **peace/war stance** indicator live-wired to `CombatState`, **movable**, and a **chrome frame** (UiNineSlicePanel drawn over content via the new `UiElement.OnDrawAfterChildren` hook). New shared widgets `UiItemSlot` (`UIElement_UIItem` 0x10000032, procedural leaf) + `UiItemList` (`UIElement_ItemList` 0x10000031, factory branch) + `IconComposer` (CPU layered composite). `CreateObject.TryParse` extended to the full ACE-order weenie-header tail to capture `IconId`/`IconOverlay`/`IconUnderlay` → `ItemRepository.EnrichItem` → re-render. Spec/plan `docs/superpowers/{specs,plans}/2026-06-16-d2b-toolbar-phase1*.md`; research drop `docs/research/2026-06-16-*deep-dive.md` + synthesis. Divergence IA-16/IA-17 added. **User-confirmed** (numbers, icons, frame). Per-task spec+code-review throughout. +- **D.5.2 — Stateful item-icon system [NEXT — handoff ready].** The full retail icon composite (`IconData::RenderIcons` @407524, 5 layers). D.5.1 built layers 1–4 (type-default underlay + custom underlay/base/overlay) + the `CreateObject` parse. **Remaining:** the effect layer (`_effects`→`GetByEnum 0x10000005`, the "item with mana vs out-of-mana" state), the overlay `ReplaceColor` tint, and **appraise-driven enrichment + icon re-composition** (overlay/effects likely arrive at Appraise `0x00C9`, not the bare `CreateObject` — capture with WireMCP first). Shared by inventory/equipment/vendor/trade — do before those panels. **Handoff: [`docs/research/2026-06-17-stateful-icon-system-handoff.md`](../research/2026-06-17-stateful-icon-system-handoff.md).** +- **D.5.3 — Toolbar interactivity [NEXT].** The toolbar is the **selected-object display**: wire the B.4 `WorldPicker`/selection state → the two hidden meters (`0x100001A1` health / `0x100001A2` mana) + the stack slider (`0x100001A4`) + the object-name line, so the bar shows what the player has selected in the world. (Click-to-use + the peace/war stance indicator already landed in D.5.1.) +- **D.5.4+ — remaining core panels.** Inventory (`gmInventoryUI`/`gmBackpackUI`), equipment/paperdoll (`gmPaperDollUI`/`gm3DItemsUI` + the `UiViewport` 3D doll), spellbook, etc. — research drop done (`docs/research/2026-06-16-*`); depends on D.5.2 (the stateful icon) + the item-slot/list spine (shipped in D.5.1) + the window manager. Deferred from D.5.1: drag/reorder, the AddShortcut/RemoveShortcut mutate wire, spell shortcuts, the faithful grip/dragbar window manager. - **D.6 — HUD.** Vital orbs (scissor-rect partial fill, dat sprites `0x060013B2`), radar (`0x06001388` / `0x06004CC1`, 1.18× range factor), compass strip (scrolling U), target name plate, damage floaters, selection indicator. See slice 06. **(Targets `AcDream.UI.Abstractions` — ships with D.2a; reskinned by D.2b.)** Phase I.2 retired the StbTrueTypeSharp `DebugOverlay` but kept `TextRenderer` + `BitmapFont` alive specifically for D.6's world-space HUD elements (damage floaters, name plates) — they need raw GL text drawing that ImGui can't reach into the 3D scene. - **D.7 — Cursor manager.** OS + dat-sourced custom cursors (`FUN_0043c1c0` GDI HCURSOR builder pattern from slice 03). **(D.2b dependency.)** - ~~**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. diff --git a/docs/research/2026-06-17-stateful-icon-system-handoff.md b/docs/research/2026-06-17-stateful-icon-system-handoff.md new file mode 100644 index 00000000..e1c952d5 --- /dev/null +++ b/docs/research/2026-06-17-stateful-icon-system-handoff.md @@ -0,0 +1,127 @@ +# Handoff — the FULL stateful item-icon system (next session) + +**Date:** 2026-06-17 +**From:** the D.5.1 toolbar session (the action bar shipped; its icon compositor is **partial**). +**Purpose:** build the **complete, retail-faithful, stateful item-icon system** — the multi-layer icon composite that reflects an item's *current state* (charged/enchanted/etc.), driven by both `CreateObject` and `Appraise`. This is **shared infrastructure**: the inventory, equipment/paperdoll, vendor, and trade panels all render item icons, so it must be solved properly once, here, before those panels are built. + +This doc is the entry point. The new-session prompt is at the bottom (§10). + +--- + +## 0. TL;DR + +A retail item icon is **not one sprite** — it's a runtime composite of **up to 5 layers** (`IconData::RenderIcons`, decomp `acclient_2013_pseudo_c.txt:407524` / `0058d180`), and **which layers apply depends on the item's live state** (item type, magic underlay, overlay tint, and the `_effects` bitfield). The D.5.1 toolbar built layers 1–4 of the composite and the `CreateObject` parse for the base/overlay/underlay ids — but the **effect layer (5), the overlay tint, and the appraise-driven state updates are missing**, which is why the user's pinned scroll still shows no overlay. The user is correct: "an item *with* mana vs *out of* mana shows a different icon" — that's exactly the stateful layer system. Build it fully. + +--- + +## 1. The retail icon model (the oracle: `IconData::RenderIcons`) + +`IconData::RenderIcons(IconData* this, ACCWeenieObject* obj)` — decomp `407524` (`0058d180`). It builds the on-screen icon by blitting layers **bottom → top** into one private 32×32 surface: + +| # | Layer | Source | Blit | Driven by | Status | +|---|---|---|---|---|---| +| 1 | **type-default underlay** (the opaque background tile) | `DBObj::GetByEnum(0x10000004, LowestSetBit(itemType)+1)`, fallback index `0x21` | `Blit_Normal` (opaque) | the item's `ItemType` | ✅ **built** (D.5.1) | +| 2 | **custom underlay** ("has magic") | `_iconUnderlayID` | `Blit_3Alpha` | item has an underlay id | ✅ parse+composite built | +| 3 | **base icon** | `_iconID` | `Blit_Normal` | always | ✅ built | +| 4 | **custom overlay** ("enchanted") | `_iconOverlayID` + `SurfaceWindow::ReplaceColor` **tint** | `Blit_3Alpha` | item has an overlay id | ⚠️ overlay sprite composited, **tint NOT applied** | +| 5 | **effect overlay** (the magic glow/state) | `DBObj::GetByEnum(0x10000005, LowestSetBit(_effects)+1)` | blit | the item's **`_effects`** bitfield (Magical/Enchanted/…) | ❌ **NOT built** | + +Plus a special case at `407546` (`0058d1ee`): **`IsThePlayer`** → `m_idIcon = GetDIDByEnum(0x10000004, 7)`, `itemType = TYPE_CONTAINER (0x200)` — the player's own paperdoll icon. Out of scope for the toolbar; **needed for the paperdoll**. + +### The enum-mapper resolve chain (already wired for 0x10000004) +`GetByEnum(enumId, index)` → `DBCache::GetDIDFromEnum` (`0x413940`): `master[enumId] → submapDID`; `submap[index] → the 0x06 RenderSurface DID`. DatReaderWriter exposes the mapper as **`EnumIDMap`** (`DB_TYPE_DID_MAPPER`); the master map DID is `_dats.Portal.Header.MasterMapId` (**= 0x25000000**, confirmed live). For the underlay: `master[0x10000004] = submap 0x25000008` (34 entries). **For the effect layer you need `master[0x10000005]`** (not yet read). `EnumIDMap.ClientEnumToID` is `IReadOnlyDictionary`; each layer DID is a `0x06` RenderSurface decoded directly by `SurfaceDecoder.DecodeRenderSurface`. + +--- + +## 2. What D.5.1 already built (read this code first) + +- **`src/AcDream.App/UI/IconComposer.cs`** — the CPU compositor. `Compose(layers)` = alpha-over, sizes to layer 0. `GetIcon(ItemType, iconId, underlayId, overlayId)` resolves the **type-default underlay** (`ResolveUnderlayDid` + `EnsureUnderlaySubMap`, via `EnumIDMap` master→`0x10000004`→submap), prepends it as the opaque layer 0, then composites custom-underlay + base + custom-overlay, caches by the `(typeUnderlayDid, iconId, underlayId, overlayId)` tuple, uploads via `TextureCache.UploadRgba8`. **Layer order + the underlay are faithful** (golden test `ResolveUnderlayDid_goldenValues_matchDat` passes against the live dat). +- **`src/AcDream.Core.Net/Messages/CreateObject.cs`** — `TryParse` now walks the **full** weenie-header optional tail (in exact ACE order, verified against `references/ACE/.../WorldObject_Networking.cs`) and captures `IconId`, `IconOverlayId` (weenieFlags `0x40000000`), `IconUnderlayId` (weenieFlags2 `0x01`). It reads `UiEffects` (weenieFlags `0x80`) but **discards it** — capturing it is part of this next phase. RestrictionDB skip is length-aware + tested. +- **`src/AcDream.Core/Items/ItemInstance.cs`** — has `IconId`, `IconUnderlayId`, `IconOverlayId`, `Type`. **No `Effects`/`UiEffects` field yet.** +- **`src/AcDream.Core/Items/ItemRepository.cs`** — `EnrichItem(objectId, iconId, name, type, iconOverlayId=0, iconUnderlayId=0)` writes the typed icon ids onto an existing item + fires `ItemPropertiesUpdated`. Threaded from `WorldSession.EntitySpawned` → `GameWindow.OnLiveEntitySpawned`. +- **`src/AcDream.App/UI/Layout/ToolbarController.cs`** — calls `iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId)` per slot, re-runs `Populate()` on `ItemRepository.ItemAdded`/`ItemPropertiesUpdated` (so a late `CreateObject` re-binds the slot's icon). +- **(related, not icon-composite)** the **slot-number** system (`SetShortcutNum`, 3 digit arrays: occupied peace/war `0x10000042`/`0x10000043` from cell composite `0x10000346`, empty/background `0x1000005e` from composite `0x10000341`) is done — it's a separate `UIElement_UIItem` feature, not the icon composite, but lives on the same widget. + +--- + +## 3. What's MISSING (the next session's work) + +1. **Layer 5 — the effect overlay (`_effects`).** Capture the item's `_effects`/`UiEffects` bitfield (CreateObject reads `UiEffects` at weenieFlags `0x80` but discards it — keep it; also it may be the appraise-only `PropertyInt.UiEffects`). Add an `Effects` field to `ItemInstance`. In `IconComposer`, resolve `GetByEnum(0x10000005, LowestSetBit(effects)+1)` (the second enum submap, `master[0x10000005]`) and composite it as the top layer. Widen `GetIcon` + the cache key to include effects. **This is the user's "mana vs out-of-mana" layer** and the most likely cause of the scroll's missing overlay (if its distinctive look is the effect glow, not a static `_iconOverlayID`). +2. **Layer 4 tint — `SurfaceWindow::ReplaceColor`.** The custom overlay is composited as a plain sprite; retail applies a per-pixel palette `ReplaceColor` tint (`407614`). Port the tint (it's a palette-index color replace — see `ACViewer TextureCache.IndexToColor` for the subpalette-overlay technique, though confirm it's the right op for icons). +3. **Appraise-driven enrichment + RE-COMPOSITION.** The icon must update when the item's icon-relevant properties change. `IdentifyObjectResponse` (`0x00C9`, `AppraiseInfoParser` / `GameEventWiring`) currently updates the `PropertyBundle` only — it does **not** update the typed `IconId/Overlay/Underlay/Effects`. Wire appraise → update those typed fields → `ItemPropertiesUpdated` → the bound widget re-resolves the icon (the cache key already changes when an id changes, so a new composite is produced). **This is the other likely cause of the scroll's blank overlay**: the overlay/effects ids may only arrive at appraise, not on the bare `CreateObject`. +4. **Settle the data-availability question (DO THIS FIRST — it's a 10-min capture).** Does ACE send `IconOverlay`/`UiEffects` on a *contained* (in-pack, un-appraised) item's `CreateObject`, or only at appraise? Capture the scroll's `0xF745 CreateObject` **and** its `0x00C9 IdentifyObjectResponse` with WireMCP (`mcp__wiremcp__*`, loopback `127.0.0.1:9000`) and log `CreateObject.Parsed.IconOverlayId/IconUnderlayId` at runtime. The answer decides whether the fix is "just build layer 5" (data already on CreateObject) or "build layer 5 + appraise enrichment" (data is appraise-gated). **Don't guess — capture.** +5. **The `IsThePlayer` container icon** (paperdoll) — `GetDIDByEnum(0x10000004, 7)` + `TYPE_CONTAINER`. Needed when the paperdoll renders the player's own icon. +6. **Identified-vs-unidentified does NOT swap the icon** (confirmed last session): appraise gates *tooltip* detail, not the base icon. So the icon layers come from the item's real props (sent on CreateObject and/or appraise), not an "identified" toggle. Don't add an appraise-gated icon variant. + +--- + +## 4. The user's framing (their words are the spec) + +> "the icon system in AC consists of several icons making up an icon. For example an item with mana has a different icon from the same item that is out of mana." + +Correct, and it maps exactly onto the model above: the **`_effects` bitfield** (and the underlay/overlay ids) reflect the item's current state, and `RenderIcons` composites the corresponding layers. "With mana vs out of mana" = the effect/underlay layers present vs absent → **the icon must re-compose when that state changes** (§3.3). Build the system so the displayed icon is always a function of the item's *current* properties, updated on every relevant property change. + +--- + +## 5. Research questions for the next session + +1. **`_effects` source + layout.** Is the icon effect bitfield the `CreateObject` `UiEffects` (weenieFlags `0x80`), the appraise `PropertyInt.UiEffects`, or both? What are its bit values (Magical/Enchanted/…)? (grep the decomp + ACE `PropertyInt`/`UiEffects` + `IconData::RenderIcons` `_effects` use at `407575`.) +2. **`master[0x10000005]` submap** — read it from the live dat (mirror the confirmed `0x10000004` resolve); enumerate its entries (index → effect-overlay `0x06` DID). Add a golden test like the underlay one. +3. **The `ReplaceColor` tint** — what color/palette does layer 4 tint with, and is it a straight palette-index replace? Cross-ref `SurfaceWindow::ReplaceColor` (decomp) + ACViewer. +4. **Appraise → icon fields** — exactly which `IdentifyObjectResponse` / `AppraiseInfo` fields carry `IconOverlay`/`IconUnderlay`/`UiEffects` (cross-ref ACE `AppraiseInfo` serialization + Chorizite). Wire them to update `ItemInstance` typed fields. +5. **Data-availability capture** (§3.4) — the WireMCP result for the scroll. +6. **Re-composition trigger** — confirm `ItemPropertiesUpdated` → widget re-resolve is sufficient (it is for the toolbar; verify the inventory/paperdoll widgets will subscribe the same way). + +--- + +## 6. References (cross-reference ≥2 per question) + +- **Named decomp** `docs/research/named-retail/acclient_2013_pseudo_c.txt`: `IconData::RenderIcons` (407524), `ACCWeenieObject::GetIconData` (408224), `DBCache::GetDIDFromEnum` (0x413940), `EnumIDMap::EnumToDID` (0x415970), `SurfaceWindow::ReplaceColor` (~407614). Headers: `acclient.h` (IconData / ACCWeenieObject struct). +- **This session's research** (the icon facts are anchored here): `docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md` (the 5-layer composite, the RenderSurface-direct decode), the D.5.1 spec/plan `docs/superpowers/{specs,plans}/2026-06-16-d2b-toolbar-phase1*.md`. +- **ACE** `references/ACE/Source/ACE.Server/WorldObjects/WorldObject_Networking.cs` (CreateObject field order), `.../Network/Structure/AppraiseInfo*.cs` (appraise fields), `ACE.Entity/Enum/PropertyInt.cs` (UiEffects). +- **ACViewer** `references/ACViewer/ACViewer/Render/TextureCache.cs` (IndexToColor / subpalette overlay) — for the layer-4 tint + icon decode. +- **Chorizite.ACProtocol** `.../Messages/` — PublicWeenieDesc + appraise field order. +- **DatReaderWriter** (nuget): `EnumIDMap` (DB_TYPE_DID_MAPPER), `RenderSurface`, `DatHeader.MasterMapId`. +- **D.2b memory crib**: `claude-memory/project_d2b_retail_ui.md` (the toolkit + the RenderSurface-vs-Surface decode gotcha; START-HERE for UI work). + +--- + +## 7. Files involved + +- `src/AcDream.App/UI/IconComposer.cs` — add the effect layer (`0x10000005`), the overlay tint, widen `GetIcon`/cache for effects. +- `src/AcDream.Core/Items/ItemInstance.cs` — add `Effects` (+ any other state fields the icon needs). +- `src/AcDream.Core.Net/Messages/CreateObject.cs` — capture `UiEffects` (already read, currently discarded) onto `Parsed`. +- `src/AcDream.Core.Net/WorldSession.cs` (`EntitySpawn` record) + `src/AcDream.App/Rendering/GameWindow.cs` (`OnLiveEntitySpawned`) — thread `UiEffects` through. +- `src/AcDream.Core/Items/ItemRepository.cs` — `EnrichItem` carry effects; **appraise enrichment** path. +- The appraise handler — `src/AcDream.Core.Net/GameEventWiring.cs` / `AppraiseInfoParser` — update typed icon fields on `0x00C9`. +- `src/AcDream.App/UI/UiItemSlot.cs` / `ToolbarController.cs` — already re-resolve on `ItemPropertiesUpdated`; no change expected (verify). + +--- + +## 8. New toolkit/API shape this introduces + +- **`IconComposer.GetIcon` becomes the single stateful icon entry point** — input is the item's full icon state `(ItemType, iconId, underlayId, overlayId, effects [, isPlayer])`; output is the composited GL texture; cache keyed by the full state tuple. Every item panel calls this. +- **`ItemInstance` carries the full icon state** (`IconId/Underlay/Overlay/Effects/Type`), updated from BOTH `CreateObject` and `Appraise`. +- **One re-composition contract**: any change to an item's icon state → `ItemRepository.ItemPropertiesUpdated` → bound `UiItemSlot` re-calls `GetIcon` (new state tuple → new composite). The toolbar already follows this; inventory/paperdoll reuse it. + +--- + +## 9. Related (separate) next toolbar work — NOT this handoff, but flagged + +The toolbar still needs **interactivity** beyond click-to-use (tracked separately in `docs/ISSUES.md`): +- It is the **selected-object display** — the two hidden meters (`0x100001A1` health / `0x100001A2` mana) + the stack slider (`0x100001A4`) + the object-name line show the object currently **selected in the world** (wire the B.4 `WorldPicker`/selection state → those elements). +- Click-to-use ✅ and peace/war stance indicator + slot-number recolor ✅ are done. +This is a distinct feature from the icon system; do the icon system first (it's the shared dependency). + +--- + +## 10. New-session prompt (paste into a fresh session) + +> Build the **FULL stateful item-icon system** for acdream (shared by inventory/equipment/vendor/trade — needed before those panels). **Read the handoff first: `docs/research/2026-06-17-stateful-icon-system-handoff.md`**, then `claude-memory/project_d2b_retail_ui.md` and `docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md`. +> +> The D.5.1 toolbar built layers 1–4 of the retail icon composite (`IconData::RenderIcons` @407524) + the `CreateObject` parse for base/overlay/underlay ids. **Missing:** the effect layer (`_effects` → `GetByEnum(0x10000005)`), the layer-4 `ReplaceColor` tint, and — critically — **appraise-driven enrichment + icon re-composition** (the overlay/effects ids likely arrive at `Appraise` (`0x00C9`), not on the bare `CreateObject`, which is why a pinned scroll shows no overlay). **First, settle the data-availability question with a WireMCP capture** of the scroll's CreateObject + IdentifyObjectResponse — don't guess. Then: capture `UiEffects` onto `ItemInstance`, read `master[0x10000005]` (mirror the working `0x10000004` underlay resolve), composite the effect layer + the overlay tint, and wire appraise → update the typed icon fields → re-compose. Follow the mandatory grep-named→cross-ref(ACE/ACViewer/Chorizite)→pseudocode→port workflow; conformance tests with golden dat values like the underlay test. The displayed icon must always be a function of the item's *current* state (the user's "item with mana vs out of mana" requirement). + +--- + +**MEMORY.md index line:** +- [Handoff: stateful item-icon system (2026-06-17)](research/2026-06-17-stateful-icon-system-handoff.md) — the full retail icon composite (`IconData::RenderIcons` @407524, 5 layers). D.5.1 built layers 1–4 + CreateObject parse (IconId/Overlay/Underlay) + the EnumIDMap `0x10000004` underlay resolve; MISSING = effect layer (`_effects`→`GetByEnum 0x10000005`, the "mana vs out-of-mana" layer), the overlay `ReplaceColor` tint, and appraise-driven enrichment+re-composition (overlay/effects likely arrive at Appraise 0x00C9, not bare CreateObject — capture with WireMCP first). Shared by inventory/equipment/vendor.