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>
16 KiB
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<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, viaEnumIDMapmaster→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 viaTextureCache.UploadRgba8. Layer order + the underlay are faithful (golden testResolveUnderlayDid_goldenValues_matchDatpasses against the live dat).src/AcDream.Core.Net/Messages/CreateObject.cs—TryParsenow walks the full weenie-header optional tail (in exact ACE order, verified againstreferences/ACE/.../WorldObject_Networking.cs) and capturesIconId,IconOverlayId(weenieFlags0x40000000),IconUnderlayId(weenieFlags20x01). It readsUiEffects(weenieFlags0x80) but discards it — capturing it is part of this next phase. RestrictionDB skip is length-aware + tested.src/AcDream.Core/Items/ItemInstance.cs— hasIconId,IconUnderlayId,IconOverlayId,Type. NoEffects/UiEffectsfield 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 + firesItemPropertiesUpdated. Threaded fromWorldSession.EntitySpawned→GameWindow.OnLiveEntitySpawned.src/AcDream.App/UI/Layout/ToolbarController.cs— callsiconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId)per slot, re-runsPopulate()onItemRepository.ItemAdded/ItemPropertiesUpdated(so a lateCreateObjectre-binds the slot's icon).- (related, not icon-composite) the slot-number system (
SetShortcutNum, 3 digit arrays: occupied peace/war0x10000042/0x10000043from cell composite0x10000346, empty/background0x1000005efrom composite0x10000341) is done — it's a separateUIElement_UIItemfeature, not the icon composite, but lives on the same widget.
3. What's MISSING (the next session's work)
- Layer 5 — the effect overlay (
_effects). Capture the item's_effects/UiEffectsbitfield (CreateObject readsUiEffectsat weenieFlags0x80but discards it — keep it; also it may be the appraise-onlyPropertyInt.UiEffects). Add anEffectsfield toItemInstance. InIconComposer, resolveGetByEnum(0x10000005, LowestSetBit(effects)+1)(the second enum submap,master[0x10000005]) and composite it as the top layer. WidenGetIcon+ 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). - Layer 4 tint —
SurfaceWindow::ReplaceColor. The custom overlay is composited as a plain sprite; retail applies a per-pixel paletteReplaceColortint (407614). Port the tint (it's a palette-index color replace — seeACViewer TextureCache.IndexToColorfor the subpalette-overlay technique, though confirm it's the right op for icons). - Appraise-driven enrichment + RE-COMPOSITION. The icon must update when the item's icon-relevant properties change.
IdentifyObjectResponse(0x00C9,AppraiseInfoParser/GameEventWiring) currently updates thePropertyBundleonly — it does not update the typedIconId/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 bareCreateObject. - Settle the data-availability question (DO THIS FIRST — it's a 10-min capture). Does ACE send
IconOverlay/UiEffectson a contained (in-pack, un-appraised) item'sCreateObject, or only at appraise? Capture the scroll's0xF745 CreateObjectand its0x00C9 IdentifyObjectResponsewith WireMCP (mcp__wiremcp__*, loopback127.0.0.1:9000) and logCreateObject.Parsed.IconOverlayId/IconUnderlayIdat 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. - The
IsThePlayercontainer icon (paperdoll) —GetDIDByEnum(0x10000004, 7)+TYPE_CONTAINER. Needed when the paperdoll renders the player's own icon. - 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
_effectssource + layout. Is the icon effect bitfield theCreateObjectUiEffects(weenieFlags0x80), the appraisePropertyInt.UiEffects, or both? What are its bit values (Magical/Enchanted/…)? (grep the decomp + ACEPropertyInt/UiEffects+IconData::RenderIcons_effectsuse at407575.)master[0x10000005]submap — read it from the live dat (mirror the confirmed0x10000004resolve); enumerate its entries (index → effect-overlay0x06DID). Add a golden test like the underlay one.- The
ReplaceColortint — what color/palette does layer 4 tint with, and is it a straight palette-index replace? Cross-refSurfaceWindow::ReplaceColor(decomp) + ACViewer. - Appraise → icon fields — exactly which
IdentifyObjectResponse/AppraiseInfofields carryIconOverlay/IconUnderlay/UiEffects(cross-ref ACEAppraiseInfoserialization + Chorizite). Wire them to updateItemInstancetyped fields. - Data-availability capture (§3.4) — the WireMCP result for the scroll.
- 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/plandocs/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, widenGetIcon/cache for effects.src/AcDream.Core/Items/ItemInstance.cs— addEffects(+ any other state fields the icon needs).src/AcDream.Core.Net/Messages/CreateObject.cs— captureUiEffects(already read, currently discarded) ontoParsed.src/AcDream.Core.Net/WorldSession.cs(EntitySpawnrecord) +src/AcDream.App/Rendering/GameWindow.cs(OnLiveEntitySpawned) — threadUiEffectsthrough.src/AcDream.Core/Items/ItemRepository.cs—EnrichItemcarry effects; appraise enrichment path.- The appraise handler —
src/AcDream.Core.Net/GameEventWiring.cs/AppraiseInfoParser— update typed icon fields on0x00C9. src/AcDream.App/UI/UiItemSlot.cs/ToolbarController.cs— already re-resolve onItemPropertiesUpdated; no change expected (verify).
8. New toolkit/API shape this introduces
IconComposer.GetIconbecomes 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.ItemInstancecarries the full icon state (IconId/Underlay/Overlay/Effects/Type), updated from BOTHCreateObjectandAppraise.- One re-composition contract: any change to an item's icon state →
ItemRepository.ItemPropertiesUpdated→ boundUiItemSlotre-callsGetIcon(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 (
0x100001A1health /0x100001A2mana) + the stack slider (0x100001A4) + the object-name line show the object currently selected in the world (wire the B.4WorldPicker/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, thenclaude-memory/project_d2b_retail_ui.mdanddocs/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) + theCreateObjectparse for base/overlay/underlay ids. Missing: the effect layer (_effects→GetByEnum(0x10000005)), the layer-4ReplaceColortint, and — critically — appraise-driven enrichment + icon re-composition (the overlay/effects ids likely arrive atAppraise(0x00C9), not on the bareCreateObject, 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: captureUiEffectsontoItemInstance, readmaster[0x10000005](mirror the working0x10000004underlay 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 1–4 + CreateObject parse (IconId/Overlay/Underlay) + the EnumIDMap0x10000004underlay resolve; MISSING = effect layer (_effects→GetByEnum 0x10000005, the "mana vs out-of-mana" layer), the overlayReplaceColortint, 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.