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

142 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 = <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)``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 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 `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.