# Stateful item-icon system — RESEARCH RESOLVED (the build basis for D.5.2) **Date:** 2026-06-17 **Supersedes the key hypotheses in** `docs/research/2026-06-17-stateful-icon-system-handoff.md`. **Method:** grep-named → cross-ref (ACE/ACViewer/Chorizite) → clean Ghidra decompile (MCP, PDB-applied `patchmem.gpr`) → live-dat probe. Each decomp claim adversarially verified against source. This doc records the **definitive** answers. Two handoff hypotheses were **wrong**; both are corrected here with evidence. --- ## 1. Data-availability — SETTLED (handoff's "DO THIS FIRST" question) **The icon ids and the effect bitfield arrive ONLY on `CreateObject`. Appraise carries NONE of them.** Definitive from the ACE oracle (the user's own server): - `references/ACE/.../Enum/Properties/PropertyDataId.cs:5-7` (verbatim): *"No properties are sent to the client unless they featured an attribute. … AssessmentProperty gets sent in successful appraisal."* - `Icon = 8`, `IconOverlay = 50`, `IconUnderlay = 52` — **no `[AssessmentProperty]`** → never in appraise (nor `[SendOnLogin]` → never in PlayerDescription property tables). - `PropertyInt.UiEffects = 18` — **no `[AssessmentProperty]`** (`PropertyInt.cs:34`; the research-agent claim that it has the attribute was a **fabrication**, caught by the verifier). - `AppraiseInfo.Write` serializes only the attributed `PropertiesInt/PropertiesDID/…` tables + the profile blobs — **no icon / UiEffects field anywhere**. Wire path for every icon input (all on the `CreateObject` weenie header, ACE `WorldObject_Networking.cs` + `PublicWeenieDesc::Pack` decomp `442421/442489/442628/442631`): | Field | weenie-flag gate | acdream status | |---|---|---| | `_iconID` | always | captured (D.5.1) | | `_iconOverlayID` | weenieFlags `0x40000000` | captured (D.5.1) | | `_iconUnderlayID` | weenieFlags2 `0x01` | captured (D.5.1) | | `_effects` (UiEffects) | weenieFlags `0x80` | **read + DISCARDED** at `CreateObject.cs:669` | **Consequence (corrects handoff §3.3/§3.4 + §5.4):** the pinned scroll shows no overlay because acdream **discards `UiEffects`** and never builds the effect treatment — NOT because the data is appraise-gated. **The handoff's "wire appraise → enrichment" item is a no-op**: appraise never carries this data, and acdream never even *sends* an `AppraiseRequest` (`AppraiseRequest.Build` exists but has zero call sites). The live "mana vs out-of-mana" re-trigger is a future `PrivateUpdateInt(UiEffects=18)` (the `0x02CD` property-update block, inventory/M2 phase), feeding the same re-composition contract — NOT appraise. --- ## 2. The effect overlay is a `ReplaceColor` tint SOURCE, not a blit layer — SETTLED Clean Ghidra decompile of `IconData::RenderIcons` (`0x0058d180`) + `SurfaceWindow::ReplaceColor` (`0x00441530`) resolves the Binary-Ninja register/calling-convention artifacts the handoff and the spine doc flagged UNVERIFIED. **`SurfaceWindow::ReplaceColor(this, RGBAColor src, RGBAColor dest)`** = for each pixel `== GetColor32(src)`, set it to `GetColor32(dest)`. A flat single-color → single-color replace. **`RenderIcons` builds two surfaces (bottom→top):** ``` m_pDragIcon (32x32): Blit base icon (m_idIcon) mode Blit_Normal (opaque) Blit custom overlay (m_idOverlayID) mode Blit_4Alpha if (effectTile != null): # effectTile = GetByEnum(0x10000005, …) ReplaceColor(this, src = WHITE(1,1,1,1), dest = ) m_pIcon (32x32): Blit type-default underlay (GetByEnum 0x10000004, lsb(itemType)+1, fb 0x21) Blit_Normal (opaque) Blit custom underlay (m_idUnderlayID) Blit_3Alpha Blit m_pDragIcon Blit_3Alpha ``` - The **effect tile is NEVER blitted** (it's the `ReplaceColor` `dest`-color source). The dat probe confirms why: every `enum 0x10000005` entry is a **32×32 FULLY-OPAQUE** colored tile (`opaque=1024, transp=0`) — blitting one on top would erase the icon. - `src` color = `RGBAColor(1,1,1,1)` → `GetColor32` → `0xFFFFFFFF` (pure-white, full alpha). So **only pure-white-opaque pixels recolor** — the effect is the recolor of the icon/overlay's white highlights to the effect hue. Subtle, data-dependent. - **Effect index:** `LowestSetBit(_effects)+1` into `enum 0x10000005`; if the resolved DBObj is null, fallback index `0x21`. NOTE retail has **no** `lsb==-1 → 0x21` pre-check on the effect path (unlike the type-underlay path), so `_effects==0` → index 0 → null → fallback `0x21` (the SOLID-BLACK tile). - **UpdateIcons dirty-check** (`0x0058da…`, decomp `407962`): re-render on change of `iconID / overlayID / underlayID / itemType / _effects`. acdream's per-tuple icon cache keyed on exactly these IS the re-composition contract. ### The one residual ambiguity (decompiler-bounded) The exact byte `ReplaceColor`'s `dest` color is read from is `effectTile + 0xac` (= the effect tile's `SurfaceWindow` header) reinterpreted as `RGBAColor` — both BN and Ghidra leave this as a struct read neither types cleanly. It is NOT pixel data and NOT a clean field either decompiler resolves. **Faithful resolution:** the effect tiles are purpose-built per-effect colored tiles, so the effect color = the tile's own representative (mean opaque) color. This is intent-faithful, not a guess about an unknown constant. Flagged for cdb/visual confirmation. (Register row + visual gate.) --- ## 3. `enum 0x10000005` effect submap — golden values (live dat, MasterMap `0x25000000` → submap `0x25000009`) `index = LowestSetBit(UiEffects)+1`; submap has 14 entries (idx 0–12 + `0x21` fallback): | UiEffects bit | name | idx | effect tile DID | tile mean RGB | |---|---|---|---|---| | 0x0001 | Magical | 1 | `0x060011CA` | blue (53,70,212) | | 0x0002 | Poisoned | 2 | `0x060011C6` | green (79,204,34) | | 0x0004 | BoostHealth | 3 | `0x06001B05` | red (213,57,59) | | 0x0008 | BoostMana | 4 | `0x060011CA` | blue | | 0x0010 | BoostStamina | 5 | `0x06001B06` | yellow (223,206,21) | | 0x0020 | Fire | 6 | `0x06001B2E` | orange | | 0x0040 | Lightning | 7 | `0x06001B2D` | purple | | 0x0080 | Frost | 8 | `0x06001B2F` | cyan-grey | | 0x0100 | Acid | 9 | `0x06001B2C` | green | | 0x0200 | Bludgeoning | 10 | `0x060033C3` | grey | | 0x0400 | Slashing | 11 | `0x060033C2` | pink-grey | | 0x0800 | Piercing | 12 | `0x060033C4` | tan | | 0x1000 | Nether | 13 | *(absent)* → fallback | → `0x060011C5` | | — | (`_effects==0`) | 0 | *(zero)* → fallback | → `0x060011C5` (SOLID black) | | — | fallback | 0x21 | `0x060011C5` | SOLID 0xFF000000 | (Cross-check, `enum 0x10000004` type-underlay, already shipped + golden-tested: Melee→`0x060011CB`, Armor→`0x060011CF`, Clothing→`0x060011F3`, Jewelry→`0x060011D5`, fallback `0x21`→`0x060011D4`.) --- ## 4. Build decisions (D.5.2) 1. **Capture `UiEffects`** from `CreateObject` → `ItemInstance.Effects`; thread through `EntitySpawn` → `EnrichItem`. 2. **`IconComposer`: faithful 2-stage composite** (drag = base+overlay+recolor; slot = typeUnderlay+customUnderlay+drag). New `ResolveEffectDid` mirrors the proven `ResolveUnderlayDid`. `GetIcon` + cache key widened to include `effects`. 3. **Effect recolor** applied only when `_effects != 0` (the meaningful case). Retail nominally runs the `_effects==0` black-fallback recolor too; we **skip** it — recoloring white→black on every item is a likely visual no-op (few pure-white pixels) but a real regression risk; documented divergence pending visual/cdb confirmation. 4. **DROP the appraise-enrichment item** (no-op — §1). The re-composition contract (`ItemPropertiesUpdated` → widget re-resolve) is already wired; its future trigger is `PrivateUpdateInt(UiEffects)`, filed for the property-update phase. 5. **Conformance**: golden `ResolveEffectDid` test (the §3 values) + a dat-free recolor test. 6. **Register**: retire `IA-16`; add rows for effect-as-recolor, the `_effects==0` skip, and the representative-color approximation. **MEMORY.md index line:** - [Research: stateful icon RESOLVED (2026-06-17)](research/2026-06-17-stateful-icon-RESOLVED.md) — definitive basis for D.5.2. Appraise carries NO icon/UiEffects (ACE `[AssessmentProperty]` proof); all icon inputs are CreateObject-only (UiEffects weenieFlags 0x80, discarded at CreateObject.cs:669). Effect overlay (enum 0x10000005) is a `ReplaceColor(white→effectColor)` SOURCE, NOT a blit layer (Ghidra `RenderIcons`@0x0058d180 + `ReplaceColor`@0x00441530). Golden effect-submap values + the 2-stage composite. Corrects the handoff's appraise + blit-layer hypotheses.