acdream/docs/research/2026-06-17-stateful-icon-RESOLVED.md
Erik 419c3ac40c docs(D.5.2): stateful item-icon spec + RESOLVED research
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>
2026-06-17 18:12:45 +02:00

8.6 KiB
Raw Permalink Blame History

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 = 52no [AssessmentProperty] → never in appraise (nor [SendOnLogin] → never in PlayerDescription property tables).
  • PropertyInt.UiEffects = 18no [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 = <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 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)GetColor320xFFFFFFFF (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 012 + 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 0x210x060011D4.)


4. Build decisions (D.5.2)

  1. Capture UiEffects from CreateObjectItemInstance.Effects; thread through EntitySpawnEnrichItem.
  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) — 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.