Frames the root cause of the live-Coldeve 4/6-missing-hotbar-icons (acdream's
enrich-existing-only item model drops CreateObjects without a pre-seeded stub) and
the retail ClientObjMaintSystem model to port. CRUX to settle first: unify the
WorldEntity + ItemRepository tracks, or keep separate with shared ingestion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Visual verification (Coldeve, Energy Crystal) showed acdream's Magical blue as a
flat tint vs retail's gradient. Root cause: RenderIcons calls the SURFACE overload
of SurfaceWindow::ReplaceColor (0x004415b0), which copies the textured effect tile
pixel-by-pixel into the icon's pure-white pixels — not the flat color->color overload
(0x00441530) I'd approximated with the tile's mean color. Port the surface overload
exactly (dst[x,y]=src[x,y] where dst==white); confirmed via clean Ghidra decompile +
named decomp. Retires AP-43 (mean-color approximation); IA-18 updated to the surface op.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Visual verification caught it: a no-mana scroll's icon edges are BLACK in retail
but rendered WHITE in acdream. Cause = the effects!=0 gate (registered AP-44) that
skipped retail's effects==0 recolor. Retail's effect tile is non-null even for
effects==0 (the 0x21 SOLID-BLACK fallback 0x060011C5), so RenderIcons recolors
pure-white pixels to black on mundane items and to the effect hue on magical ones.
Remove the gate (always recolor); retire AP-44 (now faithful). TryGetEffectColor
made internal + a golden test pins effects==0 -> ~black.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'item with mana vs out of mana' core promise: a draining item whose
UiEffects clears to 0 returns to its base icon. Guards EnrichItem +
UpdateIntProperty unconditional-assign against a future != 0 regression.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Subscribe ObjectIntPropertyUpdated (added in Group C Task 4) in GameWindow
next to the existing VitalUpdated/VitalCurrentUpdated subscriptions. Routes
PublicUpdatePropertyInt(0x02CE) UiEffects (property 18) → ItemRepository.
UpdateIntProperty → ItemInstance.Effects → ItemPropertiesUpdated → UiItemSlot
re-composites the icon in real time. The end-to-end path is the visual-
verification acceptance test (live ACE server + a draining magical item).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Widen the cache key to (typeUnderlay, icon, underlay, overlay, effects).
GetIcon is now a 2-stage composite mirroring retail IconData::RenderIcons
(0x0058d180): Stage 1 builds the drag composite (base + overlay) and,
when effects != 0, ReplaceColorWhite tints it with the effect tile's
mean-opaque color (DR-1: tint SOURCE, not blit; DR-3: zero-effects
black path skipped). Stage 2 blits typeUnderlay + custom underlay +
drag into the final cached GL texture.
Both callers updated: ToolbarController Func arity widened to 6-arg
(passes item.Effects); GameWindow closure and OnLiveEntitySpawned
EnrichItem call pass spawn.UiEffects. Tree builds with 0 warnings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ReplaceColorWhite (retail SurfaceWindow::ReplaceColor 0x00441530):
replaces only pure-white-opaque (RGBA 255,255,255,255) pixels in place.
TryGetEffectColor: resolves the effect tile DID via ResolveEffectDid,
decodes the RenderSurface, and returns the mean-opaque RGB as the tint
color (divergence DR-2: exact retail color byte is decompiler-ambiguous).
TryDecode: shared RenderSurface decode helper for the effect path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add effect-overlay submap resolve: EnsureEffectSubMap walks the portal
MasterMap (0x25000000) → EnumIDMap 0x10000005 → submap 0x25000009;
ResolveEffectDid(effects) maps LowestSetBit(effects)+1 → RenderSurface
DID with fallback to index 0x21. Golden test validates all 6 cases
(Magical/Poisoned/BoostHealth/BoostStamina/Nether/zero) against the
live dat. Retail ref: IconData::RenderIcons 0x0058d180.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New standalone parser for the server's live PropertyInt update targeting
a VISIBLE object (carries guid). Wire layout: u32 opcode + u8 sequence +
u32 guid + u32 property + i32 value (17 bytes total).
The sequence byte is parsed-past but not honored (latest-wins; DR-4).
The companion PrivateUpdatePropertyInt (0x02CD) targets the player's own
object (no guid) and is not parsed here.
Three tests: uiEffectsUpdate (round-trip guid/prop/value), wrongOpcode
(returns null), truncated (returns null on 16-byte input).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously, weenieFlags bit 0x80 (UiEffects) was read + discarded with
`pos += 4`. Now it is captured into `uiEffects` and surfaced as
`Parsed.UiEffects` — the sole wire path for the effect bitfield since
PropertyInt.UiEffects (18) has no [AssessmentProperty] and never appears
in appraise responses.
Test builder gains `uint uiEffects = 0` param; write line updated to use
it. Three new parse tests: UiEffects_Captured, UiEffectsThenIconOverlay
(cursor-arithmetic regression), and NoUiEffectsBit_LeavesUiEffectsZero.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bite-sized TDD plan for the stateful item-icon system. Corrects spec 5.8:
the live 0x02CE event binds in GameWindow (next to VitalUpdated), not
GameEventWiring (which only handles the 0xF7B0 GameEvent dispatcher).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Extensive handoff for the next session to build the full stateful item-icon system (the 5-layer IconData::RenderIcons composite + effect layer 0x10000005 + overlay ReplaceColor tint + appraise-driven enrichment/re-composition). D.5.1 toolbar flipped to SHIPPED; D.5.2 (icon system) + D.5.3 (toolbar interactivity / selected-object display) registered as next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 1: remove the [D.5.1 PROBE] bottom-right rect-dump block from the
toolbar mount in GameWindow.cs. The block iterated 7 element ids and
logged ScreenPosition/Width/Height/Type; it was marked temporary and is
now superseded by the chrome window-frame fix. The kept [D.5.1] startup
diagnostic Console.WriteLines (digit arrays, toolbar ready, window from
LayoutDesc) are untouched.
Task 2: add TryParse_HouseRestrictionsSkipped_ThenIconOverlayCaptured to
CreateObjectTests.cs. Exercises the variable-length RestrictionDB skip
(weenieFlags bit 0x04000000: 12-byte fixed header + 4-byte hash-table
header + count*8 entries) followed immediately by IconOverlay (0x40000000)
and IconUnderlay (weenieFlags2 0x01 via IncludesSecondHeader 0x04000000).
Proves the skip lands the cursor at the right position for both capture
fields. 301/301 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UiNineSlicePanel drew its full chrome in OnDraw, before children, so content painted OVER the frame. The toolbar's row-2 right cap (0x100006C0, W=8) extends 2px past the 300px content and was poking over the frame's bottom-right border (the 'missing frame' the user circled). Split the panel: center fill stays in OnDraw (background, under content); the bevel border + grip move to a new UiElement.OnDrawAfterChildren hook (foreground, over content edges) so the frame is the outermost layer. Chat is unaffected (its content is inset 5px, so the border never overlaps it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The toolbar LayoutDesc (0x21000016, 300x122) was mounted bare — no window
chrome. This commit wraps it in a UiNineSlicePanel (the same 8-piece bevel +
gold grip chrome used by the vitals and chat windows), matching the pattern at
GameWindow.cs ~line 1885 verbatim.
- toolbarFrame is the top-level UiNineSlicePanel (Draggable=true, Anchors=None
per UiNineSlicePanel ctor defaults). Outer size = 310x132 (300+2*5 x 122+2*5).
- toolbarRoot sits inside at offset (5,5) — the border thickness — with all-edge
anchors so it reflows if the frame is resized. Draggable=false, Resizable=false
on the content (only the frame is the drag handle).
- The frame's right border (x=305..310 screen) covers the row-2 right cap
overhang (~2px past the content edge at x=300..302), since the border region
starts at content_right=300 and extends to frame_right=310.
- Probe block untouched: still calls toolbarLayout.FindElement for diagnostic ids.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The empty/background digit array (property 0x1000005e) lives under cell composite 0x10000341, not 0x10000346 where peace/war are read; reading it from the wrong composite returned 0 entries so empty top-row slots showed no number. Live dat probe confirmed: 0x10000341 element 0x1000034A property 0x1000005e = 0x060010FA..0x06001102 (digits 1-9) + 0x060074CF (bottom row). Now empty top-row slots show the faint background number, occupied show the dark-box peace/war digit (decomp UIElement_UIItem::SetShortcutNum:229481/229493).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CreateObject optional-tail walker previously stopped at UseRadius (~20 fields
before IconOverlay). This left ItemInstance.IconOverlayId/IconUnderlayId always 0,
so IconComposer's underlay/overlay layers were never drawn on toolbar icons.
Exact field order verified against ACE WorldObject_Networking.cs:87-219 (the
serializer is the authority; acdream connects to a local ACE server):
UseRadius → TargetType(u32) → UiEffects(u32) → CombatUse(sbyte) →
Structure(u16) → MaxStructure(u16) → StackSize(u16) → MaxStackSize(u16) →
Container(u32) → Wielder(u32) → ValidLocations(u32) →
CurrentlyWieldedLocation(u32) → Priority(u32) → RadarBlipColor(u8) →
RadarBehavior(u8) → PScript(u16) → Workmanship(f32) → Burden(u16) →
Spell(u16) → HouseOwner(u32) → HouseRestrictions(variable RestrictionDB) →
HookItemTypes(u32) → Monarch(u32) → HookType(u16) →
IconOverlay(PackedDwordKnownType) ← CAPTURE →
IconUnderlay from weenieFlags2 bit 0x01 ← CAPTURE
RestrictionDB handled correctly: Version(u32) + OpenStatus(u32) + MonarchId(u32)
+ count(u16) + numBuckets(u16) + count×8 bytes entries. Length-aware skip, not a
fixed constant.
weenieFlags2 is now CAPTURED (not skipped) when IncludesSecondHeader
(objDescFlags bit 0x04000000) is set, so the IconUnderlay bit can be tested.
The entire extended walk is inside try/catch: truncated packets degrade to
IconOverlayId=0 / IconUnderlayId=0 (no overlay drawn), never corrupting.
Threading: CreateObject.Parsed → WorldSession.EntitySpawn → GameWindow
OnLiveEntitySpawned → Items.EnrichItem — both ids thread through all three
seams. EnrichItem extended with optional iconOverlayId + iconUnderlayId params
(defaulted 0, backward-compatible).
No change to IconComposer or ToolbarController (they already consume the ids).
Tests: 4 new CreateObject tests (IconOverlay only, overlay+underlay, no-overlay
regression, intermediate-fields cursor arithmetic). Full suite: 0 failures,
2636 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465) and
gmToolbarUI::RecvNotice_SetCombatMode (196610-196621). The 9 top-row toolbar slots
show digit labels 1-9 at all times (even when empty — confirmed from the user''s
retail screenshot). The digit sprites are real 32x32 PFID_A8R8G8B8 RenderSurfaces
with glyphs baked into the top-left corner (rest alpha=0), drawn Alphablend over
the slot icon/empty sprite.
Digit DID arrays (peace: property 0x10000042, war: 0x10000043) are read at startup
from LayoutDesc 0x21000037 element 0x1000034A under composite 0x10000346 using the
same ArrayBaseProperty{DataIdBaseProperty} pattern as LayoutImporter.ReadState.
A cited-constant fallback (same confirmed dat ids) is used if the dat navigation
fails. The war glyph set (darker/golden glyphs) switches on any combat stance;
peace glyphs (lighter) restore on NonCombat — re-stamped by RestampShortcutNumbers()
called from both Populate() and SetCombatMode().
Changes:
- UiItemSlot: ShortcutNum/ShortcutPeace/PeaceDigits/WarDigits state; SetShortcutNum/
ClearShortcutNum; OnDraw restructured (no early return) so digit draws after icon.
- ToolbarController: _peaceDigits/_warDigits/_peace fields; Bind() gains peaceDigits/
warDigits optional params; RestampShortcutNumbers() helper; Populate() and
SetCombatMode() both call RestampShortcutNumbers().
- GameWindow: reads digit arrays under _datLock from LayoutDesc 0x21000037, passes to
Bind(); cited constants as fallback.
- Tests: 5 new UiItemSlotTests (SetShortcutNum/ClearShortcutNum state); 4 new
ToolbarControllerTests (top-row/bottom-row labels, peace/war switch, array injection).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Wire IconComposer + ToolbarController.Bind + the LayoutDesc 0x21000016
import into the if (_options.RetailUi) block in GameWindow, mirroring
the vitals/chat pattern. Add UseItemByGuid helper (direct send, no
proximity gate) near the B.4b use-item path.
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>
Ports retail UIElement_ItemList (class 0x10000031) as a behavioral-leaf
container that owns its UiItemSlot children procedurally. Single-cell
default covers every toolbar slot; N-cell grid is deferred to the inventory
phase. OnDraw syncs the cell rect to the list's Width/Height each frame so
the cell is sized and hit-testable from the first rendered frame, even
though the factory sets rect AFTER construction.
Factory: adds `0x10000031u => new UiItemList(resolve)` arm before the
fallback, so all 18 toolbar itemlist slots route to UiItemList instead of
UiDatElement.
Tests: 4 new (IsLeafWidget, StartsWithOneCell, Cell_returnsFirstSlot,
Create_buildsUiItemList_forItemListClassId). All 4 pass; full suite green
(415 pass / 2 skip in App.Tests; 0 fail total).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Behavioral leaf widget for the toolbar item cell. Draws the empty-slot
sprite (0x060074CF) when unbound; draws the pre-composited icon texture
when a weenie is bound via SetItem(). ConsumesDatChildren=true prevents
the LayoutImporter from double-building the dat sub-elements. SpriteResolve
is configurable so paperdoll equip slots can swap in per-slot silhouettes
later. No Clicked/OnEvent — that wiring comes in Task 8 (ToolbarController).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two GL texture leaks plugged, both found in code review of 6e82807:
1. _handlesByRenderSurfaceId (pre-existing gap): populated by
GetOrUploadRenderSurface for UI sprites but absent from Dispose's
Phase 3 sweep. Added foreach/_gl.DeleteTexture/Clear in Dispose.
2. _adhocHandles (new): the public UploadRgba8(byte[],int,int,bool)
wrapper used by IconComposer stored composited icon handles nowhere,
so they leaked. Added _adhocHandles list; wrapper now appends the
returned GL name before returning. Dispose sweeps + clears the list.
Tracking is intentionally in the PUBLIC wrapper only — the private
UploadRgba8(DecodedTexture,bool) is shared by all keyed-cache paths
and tracking there would cause double-deletes.
No behavior change to icon rendering. No GL-context unit test added
(no context in test projects); correctness is by-inspection + green
suite (2598 passing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds IconComposer (AcDream.App.UI) which mirrors retail IconData::RenderIcons
(decomp 407524): decodes each RenderSurface layer directly via SurfaceDecoder,
composites them bottom-to-top with Porter-Duff alpha-over, and uploads the
result to a GL texture via TextureCache. Composited handles are keyed by the
(iconId, underlayId, overlayId) tuple so each unique combo is uploaded once.
Adds a public TextureCache.UploadRgba8(byte[], int, int, bool) wrapper — a
thin shell around the existing private overload — so IconComposer can upload
its CPU-side composite without duplicating any GL state logic.
Pure Compose() path is covered by 2 unit tests (opaque top wins; transparent
top preserves bottom). Dat-decode + GL-upload exercised by the visual gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add optional `onShortcuts` callback to `GameEventWiring.WireAll`; invoke
it with `parsed.Shortcuts` after the inventory/equipped loops in the
PlayerDescription handler. `GameWindow` holds the list in a new
`Shortcuts` property (initialized to empty) so the toolbar (D.5.1 Task 5)
can read hotbar slots without keeping a parser reference. Existing callers
compile unchanged — the parameter defaults to null.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Added uint IconId = 0 (defaulted, last positional param) to the EntitySpawn
record so existing call sites outside WorldSession compile unchanged. The
WorldSession invoke now passes parsed.Value.IconId as the final arg.
OnLiveEntitySpawned calls Items.EnrichItem unconditionally — it's a no-op
for non-item spawns (players/NPCs/furniture aren't in the repo), so the call
is safe for every incoming CreateObject.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds EnrichItem(objectId, iconId, name, type) — enriches an existing
stub created from PlayerDescription with the fuller data carried by its
CreateObject message. Returns false when the item isn't tracked yet
(phase 1: enrich-existing only). Raises ItemPropertiesUpdated on success
so bound widgets (the toolbar) re-render.
Two xUnit tests: enrich-existing updates IconId/Name/raises event (true),
unknown-id returns false.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ReadPackedDwordOfKnownType at the old line 516 was throwing the icon dat
id away. Declare iconId before the try-block, assign it there, and pass
IconId: iconId in the Parsed initializer so downstream UI (action bar /
equipment panels) can read the 0x06xxxxxx dat id without a separate lookup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First D.5 sub-phase: ship gmToolbarUI as the first data-driven game panel (18 slots from LayoutDesc 0x21000016, populated from the persisted PlayerDescription shortcuts, real composited icons, click-to-use). Minimal scope; faithful CPU icon pre-composite (IconData::RenderIcons port). Five bounded units: UiItemSlot, UiItemList, IconComposer, CreateObject IconId extension, ToolbarController. Roadmap registration of D.5.1 is plan step 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Five report-only deep-dives + synthesis for the next D.2b UI panels, built on the shipped widget toolkit. Confirms LayoutDesc ids (toolbar 0x21000016, inventory 0x21000023, backpack 0x21000022, paperdoll 0x21000024, 3ditems 0x21000021), the shared item-slot/item-list spine (UIElement_UIItem 0x10000032 / UIElement_ItemList 0x10000031), the 5-layer icon composite (IconData::RenderIcons @407524), the cross-panel wire catalog with acdream parse-status, and the dependency-ordered build plan.
Produced via a multi-agent research workflow; the spine agent died on a transient API error and was re-run as a focused follow-up with its decomp anchors verified against source.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deferred cosmetic polish after the widget-generalization landing: tune the
per-ChatKind transcript text colors against retail, and add pressed/hover state
feedback to the chat buttons (UiButton draws only its default state today; the
dat carries Normal/Pressed/Highlight). Not a regression — the generalized chat
matches the prior hand-made build (user-confirmed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the D.2b widget-generalization landing: generic Type-registered widgets
built by DatWidgetFactory, thin find-by-id controllers, the ConsumesDatChildren
leaf rule, Type-3-not-registered decision, and the centered-UiText vitals numbers.
Both visual gates user-confirmed; 404 tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The vitals cur/max numbers now render through the generic UiText widget — retail
gmVitalsUI uses UIElement_Text for them, not a meter-internal label. VitalsController
attaches a centered, non-interactive UiText child to each meter and stops the meter
drawing its own label (UiMeter.Label -> null). New UiText.Centered draws the first line
centered H+V with the SAME formula UiMeter's overlay used, so the numbers are
pixel-identical — user-confirmed in the live client.
This completes the D.2b widget-generalization pass: every chat + vitals widget is now
built generically and registered to its retail Type (Button/Field*/Menu/Meter/Scrollbar/
Text), with thin find-by-id controllers. (*Field is controller-placed; Type 3 stays
UiDatElement for chrome.)
Divergence register: AP-37 vitals-numbers-via-UiMeter.Label clause retired. Full suite:
404 passed, 2 skipped, 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The generalized channel menu wouldn't open: the factory recursed the Type-6
menu element's dat children, building its invisible Type-12 label child as a
UiText. Hit-testing is children-first and UiText consumes MouseDown (selection),
so the label child swallowed the menu button click and the dropdown never opened.
The transcript similarly gained an invisible Ghosted-button child (a 16x16
selection dead-zone). The old hand-made build never had these — it skipped Type 12
and hand-placed the widgets with no children.
Fix: behavioral widgets (Meter/Menu/Button/Scrollbar/Text/Field) draw their full
appearance and reproduce their dat sub-elements procedurally, so they are LEAF —
the importer must not build their dat children as separate (click-stealing)
widgets. Add UiElement.ConsumesDatChildren (default false; the 6 behavioral
widgets override true) and gate LayoutImporter recursion on it (replacing the
UiMeter-only special case). Only generic containers (UiDatElement, panels) recurse.
Visually confirmed in the live client (channel menu opens; General/Trade selected
and sent). Vitals unchanged (UiMeter was already leaf). Full suite: 404 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>