Research basis (clean Ghidra decompile via MCP + live-dat probe + ACE oracle)
overturns two handoff hypotheses:
- Appraise carries NO icon/UiEffects data (Icon/IconOverlay/IconUnderlay +
PropertyInt.UiEffects all lack [AssessmentProperty]); every icon input is
CreateObject-only. The "wire appraise -> enrichment" item is a no-op.
- The effect overlay (enum 0x10000005) is a ReplaceColor tint SOURCE, not a
blit layer (RenderIcons 0x0058d180 + ReplaceColor 0x00441530); effect tiles
are 32x32 fully-opaque colored squares.
Design (user-approved): capture UiEffects (weenieFlags 0x80, currently discarded)
-> ItemInstance.Effects; faithful 2-stage IconComposer recolor (white pixels ->
effect hue); live PublicUpdatePropertyInt(0x02CE) wire-up so the icon updates as
state changes ("item with mana vs out of mana"). Drops the appraise no-op.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.6 KiB
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.Writeserializes only the attributedPropertiesInt/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 = <color from effectTile>)
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
ReplaceColordest-color source). The dat probe confirms why: everyenum 0x10000005entry is a 32×32 FULLY-OPAQUE colored tile (opaque=1024, transp=0) — blitting one on top would erase the icon. srccolor =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)+1intoenum 0x10000005; if the resolved DBObj is null, fallback index0x21. NOTE retail has nolsb==-1 → 0x21pre-check on the effect path (unlike the type-underlay path), so_effects==0→ index 0 → null → fallback0x21(the SOLID-BLACK tile). - UpdateIcons dirty-check (
0x0058da…, decomp407962): re-render on change oficonID / 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)
- Capture
UiEffectsfromCreateObject→ItemInstance.Effects; thread throughEntitySpawn→EnrichItem. IconComposer: faithful 2-stage composite (drag = base+overlay+recolor; slot = typeUnderlay+customUnderlay+drag). NewResolveEffectDidmirrors the provenResolveUnderlayDid.GetIcon+ cache key widened to includeeffects.- Effect recolor applied only when
_effects != 0(the meaningful case). Retail nominally runs the_effects==0black-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. - DROP the appraise-enrichment item (no-op — §1). The re-composition contract
(
ItemPropertiesUpdated→ widget re-resolve) is already wired; its future trigger isPrivateUpdateInt(UiEffects), filed for the property-update phase. - Conformance: golden
ResolveEffectDidtest (the §3 values) + a dat-free recolor test. - Register: retire
IA-16; add rows for effect-as-recolor, the_effects==0skip, and the representative-color approximation.
MEMORY.md index line:
- Research: stateful icon RESOLVED (2026-06-17) — 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 aReplaceColor(white→effectColor)SOURCE, NOT a blit layer (GhidraRenderIcons@0x0058d180 +ReplaceColor@0x00441530). Golden effect-submap values + the 2-stage composite. Corrects the handoff's appraise + blit-layer hypotheses.