acdream/docs/research/2026-06-17-stateful-icon-system-handoff.md
Erik 6770381fc3 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) <noreply@anthropic.com>
2026-06-17 17:24:03 +02:00

16 KiB
Raw Blame History

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 14 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): IsThePlayerm_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<uint,uint>; 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.csTryParse 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.csEnrichItem(objectId, iconId, name, type, iconOverlayId=0, iconUnderlayId=0) writes the typed icon ids onto an existing item + fires ItemPropertiesUpdated. Threaded from WorldSession.EntitySpawnedGameWindow.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.csEnrichItem 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.

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 14 of the retail icon composite (IconData::RenderIcons @407524) + the CreateObject parse for base/overlay/underlay ids. Missing: the effect layer (_effectsGetByEnum(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) — the full retail icon composite (IconData::RenderIcons @407524, 5 layers). D.5.1 built layers 14 + CreateObject parse (IconId/Overlay/Underlay) + the EnumIDMap 0x10000004 underlay resolve; MISSING = effect layer (_effectsGetByEnum 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.