Retail IconData::RenderIcons (decomp 407524) builds the icon layer stack bottom→top:
type-default underlay (OPAQUE, Blit_Normal) first, then custom underlay, base icon,
custom overlay. acdream's IconComposer omitted the type-default underlay, leaving
filled toolbar slots with a transparent background.
Resolution via the two-level EnumIDMap chain that retail uses (DBCache::GetDIDFromEnum
0x413940): Portal.Header.MasterMapId (0x25000000) → master[0x10000004] → submap DID
(0x25000008) → submap[LSB(itemType)+1] → 0x06 RenderSurface underlay DID. Golden
values confirmed against the live dats: MeleeWeapon→0x060011CB, Armor→0x060011CF,
Clothing→0x060011F3, Jewelry→0x060011D5, None(fallback 0x21)→0x060011D4.
Changes:
- IconComposer: add ResolveUnderlayDid(ItemType)/EnsureUnderlaySubMap (memoised);
widen cache key from (uint,uint,uint)→(uint,uint,uint,uint); GetIcon gains ItemType
param and prepends the opaque underlay as layer 0 (Compose sizes to it → fully opaque)
- ToolbarController: widen _iconIds Func from 3-arg to 4-arg; Populate passes item.Type
- GameWindow: update toolbar mount lambda to 4-arg form
- Tests: update ToolbarController test stubs to (_,_,_,_); add
Compose_opaqueUnderlayFirst_resultIsFullyOpaque (dat-free) and
ResolveUnderlayDid_goldenValues_matchDat (dat-gated, skip when dats absent)
No divergence-register row existed for this omission; none added (fully ported now).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
D1 — Toolbar not movable: toolbarRoot.Anchors = AnchorEdges.None (was Left|Top)
so ApplyAnchor early-returns and doesn't re-pin the window every frame.
Matches the vitalsRoot idiom exactly.
D2 — Cannot grab toolbar by chrome: toolbarRoot.ClickThrough = false
so HitTest succeeds over the UiDatElement chrome and the drag starts.
UiDatElement ctor defaults ClickThrough=true; vitalsRoot already overrides it.
C1 — All four combat-mode indicators visible at once (war/flame stacked on
peace): ports gmToolbarUI::RecvNotice_SetCombatMode
(acclient_2013_pseudo_c.txt:196632-196669). CombatIndicatorIds[] maps
index 0-3 to NonCombat/Melee/Missile/Magic; SetCombatMode shows exactly one
and hides the other three. Default to NonCombat at bind (player always
spawns in peace). Wires CombatState.CombatModeChanged for live updates.
Tests: CombatIndicator_defaultNonCombat_onlyPeaceVisible,
CombatIndicator_setCombatModeMelee_onlyMeleeVisible,
CombatIndicator_liveSignal_updatesWhenCombatStateChanges.
V1 — Blue empty-slot square at top-left (prototype 0x100001B2 materialized):
ImportInfos now skips top-level elements that are (a) referenced as a
BaseElement by another element in the same layout AND (b) have no own state
media. The CollectBaseRefsInDesc walk covers nested children; HasNoOwnMedia
re-uses ToInfo's media extraction. The Resolve path reads BaseElement from the
raw dat via dats.Get<LayoutDesc> — it never depends on the prototype being in
the built widget tree — so the skip is safe. Conformance tests (vitals, chat)
are unaffected (they exercise Build, not ImportInfos).
Test: BuildFromInfos_PrototypeSkipped_DerivedPresent_PrototypeAbsent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port of gmToolbarUI::PostInit (slot wiring) + UpdateFromPlayerDesc (flush-and-bind
shortcuts from PlayerDescription) + SetDelayedShortcutNum (deferred ItemAdded rebind)
+ UseShortcut (click → useItem callback).
UiItemSlot gains Clicked (Action?) + OnEvent override (MouseDown → Clicked?.Invoke())
matching the retail UIElement_UIItem click dispatch pattern. UiEvent is a positional
record struct so the OnEvent override reads e.Type (int) against UiEventType.MouseDown
(const int 0x201) — confirmed from UiEvent.cs + UiText.cs before writing.
Three tests green (populate bound slot, deferred rebind on ItemAdded, click fires useItem).
Full suite: 0 failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>