acdream/docs/superpowers/plans/2026-06-16-d2b-toolbar-phase1.md
Erik 44fabd350e docs(D.5.1): toolbar phase-1 implementation plan (+ spec wiring-delta note)
12-task TDD plan: register D.5.1 -> CreateObject IconId capture -> ItemRepository.EnrichItem -> spawn-event icon wiring -> persist shortcuts -> IconComposer (CPU composite) -> UiItemSlot -> UiItemList + factory branch -> ToolbarController -> GameWindow mount -> visual gate -> bookkeeping. Concrete call sites pinned (WorldSession.cs:701 EntitySpawned, GameEventWiring.WireAll, GameWindow Items@598, BuildUse 0x0036). Synced the spec's CreateObject section with the wider-than-expected wiring found during planning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:27:49 +02:00

46 KiB
Raw Permalink Blame History

D.5.1 Toolbar (action bar) — Phase 1 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Ship the retail action bar (gmToolbarUI, LayoutDesc 0x21000016) as acdream's first data-driven game panel: 18 shortcut slots populated from the persisted PlayerDescription shortcut block, each pinned item rendering its real composited icon, with click-to-use.

Architecture: Reuse the shipped D.2b assembly pattern (dat LayoutDescLayoutImporterDatWidgetFactory → thin find-by-id controller). Two new shared widgets (UiItemSlot, UiItemList) + a CPU icon-composite pipeline (IconComposer) + the wire plumbing to carry IconId from CreateObject into ItemRepository and to persist the shortcut list. The 18 toolbar slots already resolve to UIElement_ItemList (class 0x10000031) through the dat BaseElement/BaseLayoutId chain (slot → 0x100001B20x10000339@0x2100003D, Type 0x10000031), so one DatWidgetFactory branch makes them UiItemLists automatically; the item cell is created procedurally by the list.

Tech Stack: C# .NET 10, Silk.NET OpenGL, the in-tree AcDream.App/UI retained-mode toolkit, DatCollection for RenderSurface decode, xUnit.

Spec: docs/superpowers/specs/2026-06-16-d2b-toolbar-phase1-design.md. Research anchors: docs/research/2026-06-16-ui-item-slot-icon-dragdrop-spine-deep-dive.md, docs/research/2026-06-16-action-bar-toolbar-deep-dive.md.

Spec deltas discovered during planning (elaboration, not contradiction):

  • Spec §4.4 assumed "just capture IconId." Reality: acdream's CreateObject.TryParse discards IconId (CreateObject.cs:516) AND there is no CreateObjectItemRepository wiring at all — the repo is populated only from PlayerDescription with stub ItemInstances (ObjectId+WeenieClassId). So Tasks 24 add: capture IconId, enrich the repo from the spawn event, and persist Parsed.Shortcuts (currently parsed then discarded in GameEventWiring).
  • IconId source is CONFIRMED to be CreateObject for contained pack items (ACE WorldObject_Networking.cs:79 writes WritePackedDwordOfKnownType(IconId, 0x6000000) unconditionally; Chorizite PublicWeenieDesc reads Icon with no flag gate). No fallback needed.
  • Phase-1 IconComposer scope: CPU-composite the layers whose source data ItemInstance already exposes (custom underlay IconUnderlayId + base IconId + custom overlay IconOverlayId, alpha-over). The retail IconData::RenderIcons (decomp 407524) GetByEnum type-default-underlay, the overlay ReplaceColor tint, and the effect overlay need wire data not yet parsed (overlay tint color, IconEffects) — DEFERRED with divergence rows (Task 12). This keeps Approach A (faithful CPU pre-composite) while scoping to available data.

Task 0: Register D.5.1 in the roadmap

Files:

  • Modify: docs/plans/2026-04-11-roadmap.md (the D.5 entry, ~line 433)

  • Step 1: Add the D.5.1 sub-phase entry under D.5

In docs/plans/2026-04-11-roadmap.md, immediately after the D.5 — Core panels bullet (the one at ~line 433), add:

- **D.5.1 — Toolbar (action bar) [IN PROGRESS].** First D.5 sub-phase. `gmToolbarUI` (`LayoutDesc 0x21000016`) as the first data-driven game panel: 18 shortcut slots from the persisted `PlayerDescription` SHORTCUT block, real composited icons, click-to-use. New shared widgets `UiItemSlot` (`UIElement_UIItem` 0x10000032, procedural) + `UiItemList` (`UIElement_ItemList` 0x10000031, factory-registered) + `IconComposer` (CPU 5-layer composite, `IconData::RenderIcons` @407524) + the `CreateObject``ItemRepository` IconId wiring. Spec/plan: `docs/superpowers/{specs,plans}/2026-06-16-d2b-toolbar-phase1*.md`. Deferred to later D.5 sub-phases: drag/reorder, the AddShortcut/RemoveShortcut mutate wire, meters/slider, spell shortcuts, faithful window manager, inventory, paperdoll.
  • Step 2: Commit
git add docs/plans/2026-04-11-roadmap.md
git commit -m "docs(D.5.1): register toolbar phase-1 in the roadmap"

Task 1: Capture IconId in CreateObject.Parsed

Files:

  • Modify: src/AcDream.Core.Net/Messages/CreateObject.cs (the Parsed struct ~lines 105-142; the parse at lines 515-516)

  • Test: tests/AcDream.Core.Net.Tests/CreateObjectTests.cs (add a test; create the file if no CreateObject test exists — verify with Glob tests/AcDream.Core.Net.Tests/*reate*bject*)

  • Step 1: Write the failing test

Add to tests/AcDream.Core.Net.Tests/CreateObjectTests.cs (mirror an existing CreateObject test's byte-buffer construction; if none exists, build a minimal body using the same field order as TryParse). The assertion that matters:

[Fact]
public void TryParse_capturesIconId()
{
    // A CreateObject body for a simple contained item. Build the bytes with the
    // exact field order TryParse reads (guid, ... name, packed WeenieClassId,
    // packed-of-known-type IconId 0x06xxxxxx, u32 itemType, ...). Reuse the helper
    // that an existing CreateObject test uses to assemble a body; the new assertion:
    var parsed = CreateObject.TryParse(BuildContainedItemBody(iconId: 0x06001234u));

    Assert.NotNull(parsed);
    Assert.Equal(0x06001234u, parsed!.Value.IconId);
}
  • Step 2: Run the test, verify it fails

Run: dotnet test tests/AcDream.Core.Net.Tests --filter TryParse_capturesIconId Expected: FAIL — Parsed has no member IconId (compile error), or IconId is 0.

  • Step 3: Add IconId to the Parsed struct and capture it

In src/AcDream.Core.Net/Messages/CreateObject.cs, add a field to the Parsed struct (the readonly struct around lines 105-142):

public uint IconId;        // 0x06xxxxxx RenderSurface id of the item icon (0 = none)

In TryParse, change the discard at line 516 to capture, and assign it into the returned Parsed. Replace:

_ = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix);

with:

uint iconId = ReadPackedDwordOfKnownType(body, ref pos, IconTypePrefix);

Then add IconId = iconId, to the object/struct initializer where Parsed is constructed (the return new Parsed { ... } near the end of TryParse). Leave the WeenieClassId discard at line 515 as-is for now (the spawn event already carries it separately; capturing it is out of phase-1 scope).

  • Step 4: Run the test, verify it passes

Run: dotnet test tests/AcDream.Core.Net.Tests --filter TryParse_capturesIconId Expected: PASS.

  • Step 5: Commit
git add src/AcDream.Core.Net/Messages/CreateObject.cs tests/AcDream.Core.Net.Tests/CreateObjectTests.cs
git commit -m "feat(D.5.1): capture IconId in CreateObject.Parsed (was discarded at cs:516)"

Task 2: ItemRepository.EnrichItem (icon enrichment, enrich-existing)

Files:

  • Modify: src/AcDream.Core/Items/ItemRepository.cs (add a method; events already exist at lines 49-59)

  • Test: tests/AcDream.Core.Tests/Items/ItemRepositoryTests.cs (verify the dir with Glob tests/AcDream.Core.Tests/**/*ItemRepository*; if absent, create it)

  • Step 1: Write the failing test

[Fact]
public void EnrichItem_updatesIconOnExistingStub_andRaisesUpdated()
{
    var repo = new ItemRepository();
    repo.AddOrUpdate(new ItemInstance { ObjectId = 0x5001u, WeenieClassId = 42u }); // stub from PlayerDescription
    ItemInstance? updated = null;
    repo.ItemPropertiesUpdated += i => updated = i;

    bool hit = repo.EnrichItem(0x5001u, iconId: 0x06001234u, name: "Mana Stone", type: ItemType.Misc);

    Assert.True(hit);
    Assert.Equal(0x06001234u, repo.GetItem(0x5001u)!.IconId);
    Assert.Equal("Mana Stone", repo.GetItem(0x5001u)!.Name);
    Assert.NotNull(updated);
}

[Fact]
public void EnrichItem_returnsFalse_whenItemUnknown()
{
    var repo = new ItemRepository();
    Assert.False(repo.EnrichItem(0x9999u, 0x06001234u, "x", ItemType.Misc));
}
  • Step 2: Run the test, verify it fails

Run: dotnet test tests/AcDream.Core.Tests --filter EnrichItem Expected: FAIL — EnrichItem not defined.

  • Step 3: Implement EnrichItem

Add to src/AcDream.Core/Items/ItemRepository.cs (near AddOrUpdate):

/// <summary>
/// Enrich an already-known item (a stub created from PlayerDescription) with the
/// fuller data carried by its CreateObject (icon, name, type). Returns false if the
/// item isn't tracked yet — phase 1 enriches existing items only; full
/// CreateObject ingestion of newly-acquired items is the inventory phase.
/// Raises ItemPropertiesUpdated on success so bound widgets (the toolbar) re-render.
/// </summary>
public bool EnrichItem(uint objectId, uint iconId, string name, ItemType type)
{
    if (!_items.TryGetValue(objectId, out var item)) return false;
    if (iconId != 0) item.IconId = iconId;
    if (!string.IsNullOrEmpty(name)) item.Name = name;
    if (type != default) item.Type = type;
    ItemPropertiesUpdated?.Invoke(item);
    return true;
}
  • Step 4: Run the test, verify it passes

Run: dotnet test tests/AcDream.Core.Tests --filter EnrichItem Expected: PASS (both).

  • Step 5: Commit
git add src/AcDream.Core/Items/ItemRepository.cs tests/AcDream.Core.Tests/Items/ItemRepositoryTests.cs
git commit -m "feat(D.5.1): ItemRepository.EnrichItem (icon/name/type from CreateObject)"

Task 3: Thread IconId through the spawn event into ItemRepository

This is integration wiring (no new pure unit; covered by Task 1/2 units + the visual gate). Three edits.

Files:

  • Modify: the EntitySpawn record (locate: Grep "record EntitySpawn" src/AcDream.Core.Net — likely src/AcDream.Core.Net/WorldSession.cs or a sibling)

  • Modify: src/AcDream.Core.Net/WorldSession.cs:701-719 (the EntitySpawned?.Invoke(new EntitySpawn(...)))

  • Modify: src/AcDream.App/Rendering/GameWindow.cs (the OnLiveEntitySpawned handler — subscribed at line 2216)

  • Step 1: Add IconId to the EntitySpawn record

Run: Grep "record EntitySpawn" src/AcDream.Core.Net -n. Add a uint IconId parameter to the record's positional parameter list (append it at the end to minimize call-site churn; note the one constructor call in WorldSession is updated next).

  • Step 2: Pass parsed.Value.IconId at the invoke site

In src/AcDream.Core.Net/WorldSession.cs, in the EntitySpawned?.Invoke(new EntitySpawn(...)) block (lines 701-719), add parsed.Value.IconId as the final constructor argument (matching the new record parameter position).

  • Step 3: Enrich the repo in the spawn handler

In src/AcDream.App/Rendering/GameWindow.cs, find OnLiveEntitySpawned (the handler subscribed at line 2216). Add, near the top of the handler body (after the EntitySpawn arg is in scope, call it e):

// D.5.1: enrich a known inventory/equipped item (stubbed from PlayerDescription)
// with the icon/name/type its CreateObject carries, so the toolbar can render it.
Items.EnrichItem(e.Guid, e.IconId, e.Name, e.ItemType);

(Items is the ItemRepository field at GameWindow.cs:598. EnrichItem is a no-op returning false for non-item spawns — players, NPCs, furniture — because they aren't in the repo, so this is safe to call unconditionally.)

  • Step 4: Build + run the full suite

Run: dotnet build then dotnet test Expected: green (no behavior regression; the new arg threads through).

  • Step 5: Commit
git add src/AcDream.Core.Net/WorldSession.cs src/AcDream.App/Rendering/GameWindow.cs
git commit -m "feat(D.5.1): thread CreateObject IconId into ItemRepository via spawn event"

Task 4: Persist Parsed.Shortcuts (the durable holder)

Parsed.Shortcuts is parsed in GameEventWiring.WireAll's PlayerDescription handler then discarded. Surface it to a durable holder the toolbar reads.

Files:

  • Modify: src/AcDream.Core.Net/GameEventWiring.cs (the WireAll signature + the PlayerDescription lambda)

  • Modify: src/AcDream.App/Rendering/GameWindow.cs (add a Shortcuts field; pass a callback at the WireAll(...) call ~line 2269)

  • Test: tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs

  • Step 1: Write the failing test

In tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs, add a test that feeds a PlayerDescription message carrying a SHORTCUT block through WireAll and asserts the new onShortcuts callback receives the parsed list. Mirror an existing GameEventWiringTests PlayerDescription test for the message-construction + dispatch harness:

[Fact]
public void WireAll_PlayerDescription_invokesOnShortcuts()
{
    IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>? got = null;
    // ... build the same harness an existing PD test uses, but pass the new
    // onShortcuts callback into WireAll: onShortcuts: list => got = list
    // then dispatch a PD message whose SHORTCUT block has one entry (idx=0, guid=0x5001, spell=0, layer=0).

    Assert.NotNull(got);
    Assert.Single(got!);
    Assert.Equal(0x5001u, got![0].ObjectGuid);
}
  • Step 2: Run the test, verify it fails

Run: dotnet test tests/AcDream.Core.Net.Tests --filter WireAll_PlayerDescription_invokesOnShortcuts Expected: FAIL — WireAll has no onShortcuts parameter.

  • Step 3: Add the callback to WireAll and invoke it

In src/AcDream.Core.Net/GameEventWiring.cs:

  • Add a parameter to WireAll: Action<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>>? onShortcuts = null (optional, so existing callers/tests compile unchanged).
  • In the PlayerDescription handler lambda (where Parsed is in scope, ~lines 281-433), after the existing inventory population, add:
onShortcuts?.Invoke(parsed.Shortcuts);
  • Step 4: Run the test, verify it passes

Run: dotnet test tests/AcDream.Core.Net.Tests --filter WireAll_PlayerDescription_invokesOnShortcuts Expected: PASS.

  • Step 5: Store the shortcuts in GameWindow

In src/AcDream.App/Rendering/GameWindow.cs:

  • Add a field near Items (line 598):
/// <summary>Persisted hotbar shortcuts from the last PlayerDescription (D.5.1 toolbar source).</summary>
public IReadOnlyList<AcDream.Core.Net.Messages.PlayerDescriptionParser.ShortcutEntry> Shortcuts { get; private set; }
    = System.Array.Empty<AcDream.Core.Net.Messages.PlayerDescriptionParser.ShortcutEntry>();
  • At the GameEventWiring.WireAll(...) call (~line 2269), pass onShortcuts: list => Shortcuts = list.

  • Step 6: Build + commit

Run: dotnet build then dotnet test Expected: green.

git add src/AcDream.Core.Net/GameEventWiring.cs src/AcDream.App/Rendering/GameWindow.cs tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs
git commit -m "feat(D.5.1): persist PlayerDescription shortcuts (were parsed then discarded)"

Task 5: IconComposer — CPU icon composite + cache

Files:

  • Create: src/AcDream.App/UI/IconComposer.cs
  • Modify: src/AcDream.App/Rendering/TextureCache.cs (add a public UploadRgba8 wrapper — it's currently private)
  • Test: tests/AcDream.App.Tests/UI/IconComposerTests.cs

The pure compositing core is testable; the dat-decode + GL-upload is a thin shell exercised by the visual gate.

  • Step 1: Write the failing test (pure composite)

tests/AcDream.App.Tests/UI/IconComposerTests.cs:

using AcDream.App.UI;
using Xunit;

public class IconComposerTests
{
    private static byte[] Solid(int w, int h, byte r, byte g, byte b, byte a)
    {
        var px = new byte[w * h * 4];
        for (int i = 0; i < w * h; i++) { px[i*4]=r; px[i*4+1]=g; px[i*4+2]=b; px[i*4+3]=a; }
        return px;
    }

    [Fact]
    public void Compose_alphaOver_topOpaqueLayerWins()
    {
        var bottom = (Solid(2, 2, 255, 0, 0, 255), 2, 2); // red, opaque
        var top    = (Solid(2, 2, 0, 0, 255, 255), 2, 2); // blue, opaque
        var (rgba, w, h) = IconComposer.Compose(new[] { bottom, top });
        Assert.Equal(2, w); Assert.Equal(2, h);
        Assert.Equal(0,   rgba[0]); // R
        Assert.Equal(0,   rgba[1]); // G
        Assert.Equal(255, rgba[2]); // B — top layer won
        Assert.Equal(255, rgba[3]); // A
    }

    [Fact]
    public void Compose_alphaOver_transparentTopKeepsBottom()
    {
        var bottom = (Solid(1, 1, 255, 0, 0, 255), 1, 1);
        var top    = (Solid(1, 1, 0, 0, 255, 0), 1, 1); // fully transparent blue
        var (rgba, _, _) = IconComposer.Compose(new[] { bottom, top });
        Assert.Equal(255, rgba[0]); // bottom red preserved
        Assert.Equal(0,   rgba[2]);
    }
}
  • Step 2: Run the test, verify it fails

Run: dotnet test tests/AcDream.App.Tests --filter IconComposer Expected: FAIL — IconComposer not defined.

  • Step 3: Implement IconComposer

src/AcDream.App/UI/IconComposer.cs:

using System;
using System.Collections.Generic;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.DBObjs;

namespace AcDream.App.UI;

/// <summary>
/// Builds an item icon by alpha-compositing its RenderSurface layers into one 32×32
/// texture, mirroring retail IconData::RenderIcons (decomp 407524). Each layer is a
/// 0x06 RenderSurface decoded DIRECTLY (the D.2b RenderSurface-vs-Surface rule).
/// Phase 1 composites the layers ItemInstance exposes (custom underlay + base +
/// custom overlay); the GetByEnum type-default underlay, the overlay ReplaceColor
/// tint, and the effect overlay are deferred (see plan Task 12 / divergence rows).
/// Composited textures are cached by their layer-id tuple.
/// </summary>
public sealed class IconComposer
{
    private readonly DatCollection _dats;
    private readonly TextureCache _cache;
    private readonly Dictionary<(uint, uint, uint), uint> _byTuple = new();

    public IconComposer(DatCollection dats, TextureCache cache)
    {
        _dats = dats;
        _cache = cache;
    }

    /// <summary>Pure alpha-over composite, bottom→top. Layers may differ in size;
    /// the result is sized to the FIRST (bottom) layer and upper layers are sampled
    /// top-left aligned (all icon layers are 32×32 in practice).</summary>
    public static (byte[] rgba, int w, int h) Compose(IReadOnlyList<(byte[] rgba, int w, int h)> layers)
    {
        if (layers.Count == 0) return (Array.Empty<byte>(), 0, 0);
        var (baseRgba, w, h) = layers[0];
        var outp = (byte[])baseRgba.Clone();
        for (int li = 1; li < layers.Count; li++)
        {
            var (src, sw, sh) = layers[li];
            int cw = Math.Min(w, sw), ch = Math.Min(h, sh);
            for (int y = 0; y < ch; y++)
            for (int x = 0; x < cw; x++)
            {
                int di = (y * w + x) * 4, si = (y * sw + x) * 4;
                float sa = src[si + 3] / 255f;
                if (sa <= 0f) continue;
                float da = 1f - sa;
                outp[di]     = (byte)(src[si]     * sa + outp[di]     * da);
                outp[di + 1] = (byte)(src[si + 1] * sa + outp[di + 1] * da);
                outp[di + 2] = (byte)(src[si + 2] * sa + outp[di + 2] * da);
                outp[di + 3] = (byte)Math.Min(255f, src[si + 3] + outp[di + 3] * da);
            }
        }
        return (outp, w, h);
    }

    /// <summary>Resolve (and cache) the composited GL texture for an item's icon
    /// layers. Returns 0 if no base icon is available.</summary>
    public uint GetIcon(uint iconId, uint underlayId, uint overlayId)
    {
        if (iconId == 0) return 0;
        var key = (iconId, underlayId, overlayId);
        if (_byTuple.TryGetValue(key, out var tex)) return tex;

        var layers = new List<(byte[] rgba, int w, int h)>();
        AddLayer(layers, underlayId);
        AddLayer(layers, iconId);
        AddLayer(layers, overlayId);
        if (layers.Count == 0) return 0;

        var (rgba, w, h) = Compose(layers);
        uint handle = _cache.UploadRgba8(rgba, w, h, nearest: true);
        _byTuple[key] = handle;
        return handle;
    }

    private void AddLayer(List<(byte[], int, int)> layers, uint renderSurfaceId)
    {
        if (renderSurfaceId == 0) return;
        if (!_dats.Portal.TryGet<RenderSurface>(renderSurfaceId, out var rs) &&
            !_dats.HighRes.TryGet<RenderSurface>(renderSurfaceId, out rs))
            return;
        var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette: null);
        layers.Add((decoded.Rgba8, decoded.Width, decoded.Height));
    }
}
  • Step 4: Add the public UploadRgba8 wrapper to TextureCache

In src/AcDream.App/Rendering/TextureCache.cs, expose the existing private upload (the one GetOrUploadRenderSurface calls). Add:

/// <summary>Upload raw RGBA8 bytes as a GL texture (used by IconComposer for
/// CPU-composited icons). Returns the GL handle.</summary>
public uint UploadRgba8(byte[] rgba, int width, int height, bool nearest)
    => UploadRgba8Internal(rgba, width, height, nearest); // rename the existing private method to *Internal if needed, or call it directly if it already has this shape

(Verify the existing private upload's name/signature with Grep "UploadRgba8" src/AcDream.App/Rendering/TextureCache.cs; if it already takes (byte[], int, int, bool), just change its accessibility to public instead of adding a wrapper.)

  • Step 5: Run the tests, verify they pass; build

Run: dotnet test tests/AcDream.App.Tests --filter IconComposer then dotnet build Expected: PASS + green build.

  • Step 6: Commit
git add src/AcDream.App/UI/IconComposer.cs src/AcDream.App/Rendering/TextureCache.cs tests/AcDream.App.Tests/UI/IconComposerTests.cs
git commit -m "feat(D.5.1): IconComposer — CPU alpha-over icon composite + cache"

Task 6: UiItemSlot widget (the item cell)

Files:

  • Create: src/AcDream.App/UI/UiItemSlot.cs

  • Test: tests/AcDream.App.Tests/UI/UiItemSlotTests.cs

  • Step 1: Write the failing test

using AcDream.App.UI;
using Xunit;

public class UiItemSlotTests
{
    [Fact]
    public void IsLeafWidget()
        => Assert.True(new UiItemSlot().ConsumesDatChildren);

    [Fact]
    public void DefaultEmptySprite_isToolbarBorder()
        => Assert.Equal(0x060074CFu, new UiItemSlot().EmptySprite);

    [Fact]
    public void Empty_whenNoItem()
    {
        var s = new UiItemSlot();
        Assert.Equal(0u, s.ItemId);
        Assert.Equal(0u, s.IconTexture);
    }

    [Fact]
    public void SetItem_setsIdAndTexture()
    {
        var s = new UiItemSlot();
        s.SetItem(0x5001u, 0x99u);
        Assert.Equal(0x5001u, s.ItemId);
        Assert.Equal(0x99u, s.IconTexture);
    }
}
  • Step 2: Run the test, verify it fails

Run: dotnet test tests/AcDream.App.Tests --filter UiItemSlot Expected: FAIL — UiItemSlot not defined.

  • Step 3: Implement UiItemSlot

src/AcDream.App/UI/UiItemSlot.cs:

using System;
using System.Numerics;

namespace AcDream.App.UI;

/// <summary>
/// One item-in-a-slot cell (port of retail UIElement_UIItem, class 0x10000032).
/// A behavioral LEAF: it draws the empty-slot sprite when unbound, else a
/// pre-composited icon texture (set by the controller). Holds the bound weenie
/// guid (retail UIElement_UIItem::itemID, +0x5FC).
/// </summary>
public sealed class UiItemSlot : UiElement
{
    public UiItemSlot() { ClickThrough = false; }

    public override bool ConsumesDatChildren => true;

    /// <summary>Bound weenie guid (0 = empty). Retail UIElement_UIItem::itemID.</summary>
    public uint ItemId { get; private set; }

    /// <summary>Pre-composited icon GL texture for the bound item (0 = none).</summary>
    public uint IconTexture { get; private set; }

    /// <summary>Empty-slot sprite. Default = the generic toolbar empty-slot border
    /// 0x060074CF (uiitem template 0x21000037, state ItemSlot_Empty). Configurable so
    /// paperdoll equip slots can use their per-slot silhouettes later.</summary>
    public uint EmptySprite { get; set; } = 0x060074CFu;

    /// <summary>RenderSurface id → (GL texture, w, h). Set by the factory/controller.</summary>
    public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }

    public void SetItem(uint itemId, uint iconTexture)
    {
        ItemId = itemId;
        IconTexture = iconTexture;
    }

    public void Clear() { ItemId = 0; IconTexture = 0; }

    protected override void OnDraw(UiRenderContext ctx)
    {
        if (ItemId != 0 && IconTexture != 0)
        {
            ctx.DrawSprite(IconTexture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
            return;
        }
        if (SpriteResolve is not null && EmptySprite != 0)
        {
            var (tex, _, _) = SpriteResolve(EmptySprite);
            if (tex != 0)
                ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
        }
    }
}
  • Step 4: Run the tests, verify they pass

Run: dotnet test tests/AcDream.App.Tests --filter UiItemSlot Expected: PASS (all four).

  • Step 5: Commit
git add src/AcDream.App/UI/UiItemSlot.cs tests/AcDream.App.Tests/UI/UiItemSlotTests.cs
git commit -m "feat(D.5.1): UiItemSlot widget (UIElement_UIItem cell port)"

Task 7: UiItemList widget + DatWidgetFactory branch

Files:

  • Create: src/AcDream.App/UI/UiItemList.cs

  • Modify: src/AcDream.App/UI/Layout/DatWidgetFactory.cs (the Create switch, lines 63-71)

  • Test: tests/AcDream.App.Tests/UI/UiItemListTests.cs + tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs (verify the factory-test file exists with Glob tests/AcDream.App.Tests/**/*WidgetFactory*; if absent, create it)

  • Step 1: Write the failing tests

tests/AcDream.App.Tests/UI/UiItemListTests.cs:

using AcDream.App.UI;
using Xunit;

public class UiItemListTests
{
    [Fact]
    public void IsLeafWidget() => Assert.True(new UiItemList().ConsumesDatChildren);

    [Fact]
    public void StartsWithOneCell_forSingleCellSlot()
    {
        var list = new UiItemList();
        Assert.Equal(1, list.GetNumUIItems());
        Assert.NotNull(list.GetItem(0));
    }

    [Fact]
    public void Cell_returnsTheFirstSlot()
    {
        var list = new UiItemList();
        Assert.Same(list.GetItem(0), list.Cell);
    }
}

Add to the factory test file:

[Fact]
public void Create_buildsUiItemList_forItemListClassId()
{
    var info = new AcDream.App.UI.Layout.ElementInfo { Id = 0x100001A7u, Type = 0x10000031u, Width = 32, Height = 32 };
    var w = AcDream.App.UI.Layout.DatWidgetFactory.Create(info, _ => (0u, 0, 0), null);
    Assert.IsType<AcDream.App.UI.UiItemList>(w);
}
  • Step 2: Run the tests, verify they fail

Run: dotnet test tests/AcDream.App.Tests --filter "UiItemList|UiItemList_forItemListClassId" Expected: FAIL — UiItemList not defined / factory returns UiDatElement.

  • Step 3: Implement UiItemList

src/AcDream.App/UI/UiItemList.cs:

using System;
using System.Collections.Generic;

namespace AcDream.App.UI;

/// <summary>
/// A container of item cells (port of retail UIElement_ItemList, class 0x10000031).
/// Behavioral LEAF: it creates/owns its UiItemSlot children procedurally, so the
/// LayoutImporter must NOT build dat children. The toolbar uses single-cell
/// instances (one slot); the inventory phase will grow this to an N-cell grid.
/// </summary>
public sealed class UiItemList : UiElement
{
    private readonly List<UiItemSlot> _cells = new();

    public UiItemList(Func<uint, (uint tex, int w, int h)>? spriteResolve = null)
    {
        SpriteResolve = spriteResolve;
        // Single-cell default: every toolbar slot always shows one cell (empty or filled).
        AddItem(new UiItemSlot { SpriteResolve = spriteResolve });
    }

    public override bool ConsumesDatChildren => true;

    public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }

    /// <summary>Convenience for single-cell slots (the toolbar): the first cell.</summary>
    public UiItemSlot Cell => _cells[0];

    public int GetNumUIItems() => _cells.Count;

    public UiItemSlot? GetItem(int index)
        => index >= 0 && index < _cells.Count ? _cells[index] : null;

    public void AddItem(UiItemSlot cell)
    {
        cell.SpriteResolve ??= SpriteResolve;
        cell.Left = 0; cell.Top = 0; cell.Width = Width; cell.Height = Height;
        _cells.Add(cell);
        AddChild(cell);
    }

    public void Flush()
    {
        foreach (var c in _cells) RemoveChild(c);
        _cells.Clear();
    }

    protected override void OnDraw(UiRenderContext ctx)
    {
        // The factory sets THIS list's Width/Height AFTER construction, so the cell
        // (added in the ctor) starts 0x0. For the single-cell toolbar slot, keep the
        // cell sized to the list each frame; the cell paints itself in the children
        // pass that follows. (N-cell grid layout is the inventory phase.)
        if (_cells.Count > 0)
        {
            var cell = _cells[0];
            cell.Left = 0; cell.Top = 0; cell.Width = Width; cell.Height = Height;
        }
    }
}

Note: ConsumesDatChildren stops the IMPORTER from adding dat children, but the list still draws its own UiItemSlot children (added via AddChild) through the normal DrawSelfAndChildren traversal — ConsumesDatChildren only gates the importer, not runtime children. The cell's Width/Height are synced in the list's OnDraw (which runs before the children pass), so the cell is correctly sized + hit-testable from the first rendered frame.

  • Step 4: Add the factory branch

In src/AcDream.App/UI/Layout/DatWidgetFactory.cs, add to the Create switch (lines 63-71), before the _ fallback:

0x10000031u => new UiItemList(resolve),   // UIElement_ItemList — toolbar/inventory/paperdoll slots

(The item cell class 0x10000032 is created procedurally by UiItemList, not via a static dat element in the toolbar, so it needs no factory branch this phase.)

  • Step 5: Run the tests, verify they pass; build

Run: dotnet test tests/AcDream.App.Tests --filter "UiItemList|ItemListClassId" then dotnet build Expected: PASS + green.

  • Step 6: Commit
git add src/AcDream.App/UI/UiItemList.cs src/AcDream.App/UI/Layout/DatWidgetFactory.cs tests/AcDream.App.Tests/UI/UiItemListTests.cs tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs
git commit -m "feat(D.5.1): UiItemList widget + factory branch for class 0x10000031"

Task 8: ToolbarController (the gmToolbarUI::PostInit analogue)

Files:

  • Create: src/AcDream.App/UI/Layout/ToolbarController.cs

  • Test: tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs

  • Step 1: Write the failing test

using System;
using System.Collections.Generic;
using AcDream.App.UI;
using AcDream.App.UI.Layout;
using AcDream.Core.Items;
using AcDream.Core.Net.Messages;
using Xunit;

public class ToolbarControllerTests
{
    private static readonly uint[] Row1 =
        { 0x100001A7,0x100001A8,0x100001A9,0x100001AA,0x100001AB,0x100001AC,0x100001AD,0x100001AE,0x100001AF };
    private static readonly uint[] Row2 =
        { 0x100006B7,0x100006B8,0x100006B9,0x100006BA,0x100006BB,0x100006BC,0x100006BD,0x100006BE,0x100006BF };

    private static (ImportedLayout layout, Dictionary<uint, UiItemList> slots) FakeToolbar()
    {
        var dict = new Dictionary<uint, UiElement>();
        var slots = new Dictionary<uint, UiItemList>();
        var root = new UiPanel();
        foreach (var id in Row1) AddSlot(id);
        foreach (var id in Row2) AddSlot(id);
        return (new ImportedLayout(root, dict), slots);

        void AddSlot(uint id)
        {
            var list = new UiItemList(_ => (0u, 0, 0)) { Width = 32, Height = 32 };
            dict[id] = list; slots[id] = list; root.AddChild(list);
        }
    }

    [Fact]
    public void Populate_bindsShortcutToCorrectSlot()
    {
        var (layout, slots) = FakeToolbar();
        var repo = new ItemRepository();
        repo.AddOrUpdate(new ItemInstance { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
        var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
        { new(Index: 0, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };

        ToolbarController.Bind(layout, repo, () => shortcuts,
            iconIds: (_,_,_) => 0x77u, useItem: _ => { });

        Assert.Equal(0x5001u, slots[Row1[0]].Cell.ItemId);
        Assert.Equal(0x77u,   slots[Row1[0]].Cell.IconTexture);
        Assert.Equal(0u,      slots[Row1[1]].Cell.ItemId); // others empty
    }

    [Fact]
    public void DeferredRebind_whenItemArrivesLate()
    {
        var (layout, slots) = FakeToolbar();
        var repo = new ItemRepository(); // item NOT present yet
        var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
        { new(Index: 2, ObjectGuid: 0x5002u, SpellId: 0, Layer: 0) };

        ToolbarController.Bind(layout, repo, () => shortcuts,
            iconIds: (_,_,_) => 0x88u, useItem: _ => { });
        Assert.Equal(0u, slots[Row1[2]].Cell.ItemId); // not bound yet

        repo.AddOrUpdate(new ItemInstance { ObjectId = 0x5002u, WeenieClassId = 1u, IconId = 0x06005678u });

        Assert.Equal(0x5002u, slots[Row1[2]].Cell.ItemId); // rebound on ItemAdded
    }

    [Fact]
    public void Click_emitsUseForBoundItem()
    {
        var (layout, slots) = FakeToolbar();
        var repo = new ItemRepository();
        repo.AddOrUpdate(new ItemInstance { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
        var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
        { new(Index: 0, ObjectGuid: 0x5001u, SpellId: 0, Layer: 0) };
        uint used = 0;

        ToolbarController.Bind(layout, repo, () => shortcuts,
            iconIds: (_,_,_) => 0x77u, useItem: g => used = g);
        slots[Row1[0]].Cell.OnEvent(new UiEvent { Type = UiEventType.MouseDown });

        Assert.Equal(0x5001u, used);
    }
}

(Adapt UiEvent construction + the click-emit seam to the toolkit's actual event shape — see Step 3; if UiItemSlot needs a Clicked callback rather than handling OnEvent, wire that in Step 3 and update this assertion to invoke it.)

  • Step 2: Run the tests, verify they fail

Run: dotnet test tests/AcDream.App.Tests --filter ToolbarController Expected: FAIL — ToolbarController not defined.

  • Step 3: Implement ToolbarController

src/AcDream.App/UI/Layout/ToolbarController.cs:

using System;
using System.Collections.Generic;
using AcDream.Core.Items;
using AcDream.Core.Net.Messages;

namespace AcDream.App.UI.Layout;

/// <summary>
/// Binds the imported gmToolbarUI window (LayoutDesc 0x21000016) to live data —
/// the gm*UI::PostInit analogue. Finds the 18 shortcut slots (UiItemList) by id,
/// populates them from the persisted PlayerDescription shortcuts (UpdateFromPlayerDesc),
/// re-binds deferred slots when an item's CreateObject arrives (SetDelayedShortcutNum),
/// and on click uses the bound item (UseShortcut → ItemHolder::UseObject → use-item).
/// </summary>
public sealed class ToolbarController
{
    // Slot element ids, in slot-index order (toolbar pre-dump 0x21000016).
    private static readonly uint[] SlotIds =
    {
        0x100001A7,0x100001A8,0x100001A9,0x100001AA,0x100001AB,0x100001AC,0x100001AD,0x100001AE,0x100001AF,
        0x100006B7,0x100006B8,0x100006B9,0x100006BA,0x100006BB,0x100006BC,0x100006BD,0x100006BE,0x100006BF,
    };
    // Hidden-by-default elements (gmToolbarUI::PostInit): selected-object meters + stack slider.
    private static readonly uint[] HiddenIds = { 0x100001A1, 0x100001A2, 0x100001A4 };

    private readonly UiItemList?[] _slots = new UiItemList?[SlotIds.Length];
    private readonly ItemRepository _repo;
    private readonly Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> _shortcuts;
    private readonly Func<uint, uint, uint, uint> _iconIds; // (iconId, underlay, overlay) → GL texture
    private readonly Action<uint> _useItem;

    private ToolbarController(ImportedLayout layout, ItemRepository repo,
        Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
        Func<uint, uint, uint, uint> iconIds, Action<uint> useItem)
    {
        _repo = repo; _shortcuts = shortcuts; _iconIds = iconIds; _useItem = useItem;

        for (int i = 0; i < SlotIds.Length; i++)
        {
            _slots[i] = layout.FindElement(SlotIds[i]) as UiItemList;
            if (_slots[i] is { } list)
                WireClick(list);
        }
        foreach (var id in HiddenIds)
            if (layout.FindElement(id) is { } e) e.Visible = false;

        repo.ItemAdded            += _ => Populate();
        repo.ItemPropertiesUpdated += _ => Populate();
    }

    public static ToolbarController Bind(ImportedLayout layout, ItemRepository repo,
        Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
        Func<uint, uint, uint, uint> iconIds, Action<uint> useItem)
    {
        var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem);
        c.Populate();
        return c;
    }

    /// <summary>Port of gmToolbarUI::UpdateFromPlayerDesc — flush then bind each shortcut.</summary>
    public void Populate()
    {
        foreach (var list in _slots) list?.Cell.Clear();

        foreach (var sc in _shortcuts())
        {
            if (sc.ObjectGuid == 0) continue;            // spell shortcuts — deferred phase
            if (sc.Index >= _slots.Length) continue;
            var list = _slots[(int)sc.Index];
            if (list is null) continue;
            var item = _repo.GetItem(sc.ObjectGuid);
            if (item is null) continue;                  // SetDelayedShortcutNum: re-bound on ItemAdded
            uint tex = _iconIds(item.IconId, item.IconUnderlayId, item.IconOverlayId);
            list.Cell.SetItem(sc.ObjectGuid, tex);
        }
    }

    private void WireClick(UiItemList list)
    {
        list.Cell.Clicked = () =>
        {
            if (list.Cell.ItemId != 0) _useItem(list.Cell.ItemId);
        };
    }
}

This requires a Clicked callback on UiItemSlot. Add to UiItemSlot (Task 6 file) and have OnEvent invoke it on mouse-down:

public Action? Clicked { get; set; }

public override bool OnEvent(in UiEvent e)
{
    if (e.Type == UiEventType.MouseDown) { Clicked?.Invoke(); return true; }
    return false;
}

(If UiEvent/UiEventType member names differ, match the toolkit's actual definitions — Grep "enum UiEventType" src/AcDream.App/UI.)

  • Step 4: Run the tests, verify they pass; build

Run: dotnet test tests/AcDream.App.Tests --filter ToolbarController then dotnet build Expected: PASS (all three) + green.

  • Step 5: Commit
git add src/AcDream.App/UI/Layout/ToolbarController.cs src/AcDream.App/UI/UiItemSlot.cs tests/AcDream.App.Tests/UI/Layout/ToolbarControllerTests.cs
git commit -m "feat(D.5.1): ToolbarController — bind 18 slots, populate, deferred rebind, click-to-use"

Task 9: Wire the toolbar into GameWindow

Integration (covered by the visual gate). Mirror the vitals import + mount.

Files:

  • Modify: src/AcDream.App/Rendering/GameWindow.cs (the if (_options.RetailUi) block, ~lines 1761-1898, after the chat block)

  • Step 1: Construct the IconComposer once

In the if (_options.RetailUi) block (after _uiHost + the sprite resolver ResolveChrome exist, ~line 1778), add:

var iconComposer = new AcDream.App.UI.IconComposer(_dats!, cache);
  • Step 2: Import the toolbar layout + bind the controller

After the chat block (~line 1898), add (mirroring the vitals import at 1800-1828):

AcDream.App.UI.Layout.ImportedLayout? toolbarLayout;
lock (_datLock)
    toolbarLayout = AcDream.App.UI.Layout.LayoutImporter.Import(
        _dats!, 0x21000016u, ResolveChrome, vitalsDatFont);
if (toolbarLayout is not null)
{
    AcDream.App.UI.Layout.ToolbarController.Bind(
        toolbarLayout, Items,
        () => Shortcuts,
        iconIds: (icon, under, over) => iconComposer.GetIcon(icon, under, over),
        useItem: guid => UseItemByGuid(guid));   // existing use-item path (see Step 3)

    var toolbarRoot = toolbarLayout.Root;
    toolbarRoot.Left = 10; toolbarRoot.Top = 300;     // initial position; user-draggable
    toolbarRoot.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top;
    toolbarRoot.Draggable = true;
    _uiHost.Root.AddChild(toolbarRoot);
}
  • Step 3: Provide UseItemByGuid

acdream already builds + sends use-item at GameWindow.cs:11577-11579 (InteractRequests.BuildUse(seq, guid)_liveSession.SendGameAction). Extract that into a small helper if it isn't already callable by guid:

private void UseItemByGuid(uint guid)
{
    if (_liveSession is null) return;
    var seq  = _liveSession.NextGameActionSequence();
    var body = AcDream.Core.Net.Messages.InteractRequests.BuildUse(seq, guid);
    _liveSession.SendGameAction(body);
}

(If a guid-based use helper already exists near line 11577, call it instead of duplicating.)

  • Step 4: Build

Run: dotnet build Expected: green.

  • Step 5: Commit
git add src/AcDream.App/Rendering/GameWindow.cs
git commit -m "feat(D.5.1): mount the toolbar window under ACDREAM_RETAIL_UI"

Task 10: Full suite + manual smoke gate

  • Step 1: Build + full test suite

Run: dotnet build then dotnet test Expected: all green.

  • Step 2: Launch + visual verification (the user's gate)

Launch per CLAUDE.md (PowerShell, background, Tee to launch.log):

$env:ACDREAM_DAT_DIR   = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE      = "1"
$env:ACDREAM_RETAIL_UI = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"; $env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"; $env:ACDREAM_TEST_PASS = "testpassword"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 | Tee-Object -FilePath "launch.log"

Acceptance (user confirms by looking):

  • An 18-slot action bar (2 rows of 9) renders with the dat chrome + empty-slot sprites.
  • Any persisted +Acdream shortcuts show their real composited item icons.
  • Clicking a pinned item uses it (observe server-side / in-world effect).
  • The bar drags as a whole window.

(If +Acdream has no persisted shortcuts, the empty-slot render is still a valid gate; pinning real items to test icons may need the inventory phase or a server-side pre-pin — note this to the user.)


Task 11: Bookkeeping — divergence register, roadmap shipped, memory

Files:

  • Modify: docs/architecture/retail-divergence-register.md

  • Modify: docs/plans/2026-04-11-roadmap.md

  • Modify: claude-memory/project_d2b_retail_ui.md (durable lesson, if any)

  • Step 1: Add divergence rows

Add rows to docs/architecture/retail-divergence-register.md for the phase-1 icon deferrals + the empty-sprite constant:

  • Icon composite omits the retail GetByEnum type-default underlay (IconData::RenderIcons 407524, enum 0x10000004), the overlay ReplaceColor tint, and the effect overlay (enum 0x10000005) — their source data (overlay tint color, IconEffects) isn't parsed yet. Risk: items with a material/effect overlay render without it. Retire when the inventory phase parses the full PublicWeenieDesc.

  • UiItemSlot.EmptySprite defaults to the constant 0x060074CF instead of importing the empty-slot state from the uiitem template 0x21000037. Risk: paperdoll equip-slot silhouettes need per-slot empty sprites (already configurable). Retire when the cell imports its template states.

  • (Reuse the existing IA-12 row for whole-window-drag — no new row.)

  • Step 2: Flip the roadmap entry to shipped

In docs/plans/2026-04-11-roadmap.md, change the D.5.1 entry from [IN PROGRESS] to ✓ SHIPPED with the commit range, mirroring the other D.2b shipped entries.

  • Step 3: Commit
git add docs/architecture/retail-divergence-register.md docs/plans/2026-04-11-roadmap.md claude-memory/project_d2b_retail_ui.md
git commit -m "docs(D.5.1): divergence rows + roadmap shipped + memory for the toolbar"

Task 12 (FOLLOW-UP, optional within phase): faithful icon layers

Deferred from Task 5 to keep phase 1 shippable; do only if the toolbar icons visibly lack the standard background. NOT required for the phase-1 acceptance gate.

  • Port DBObj::GetByEnum(0x10000004, lsb(itemType)+1) (the type-default underlay) — first confirm what 0x10000004 maps to (dump an EnumMapper DBObj) to decide whether it's the universal icon background. Add it as the bottom layer in IconComposer.GetIcon.
  • Parse IconEffects/IconOverlay tint from CreateObject (extend CreateObject.Parsed + ItemInstance), then add the ReplaceColor overlay tint + the effect overlay (GetByEnum 0x10000005).
  • Delete the corresponding divergence rows from Task 11 Step 1 as each layer lands.

Self-review notes (author)

  • Spec coverage: spec §2 widgets → Tasks 6,7; §4.3 icon → Task 5 (+12); §4.4 CreateObject/ItemInstance → Tasks 1,2,3; §4.5 ToolbarController → Task 8; §4.6 wiring/gating → Task 9; §5 testing → per-task TDD + Task 10; §6 acceptance → Task 10; §8 bookkeeping → Tasks 0,11. The shortcut-holder (Task 4) was implicit in §4.5's "reads Parsed.Shortcuts" and is made explicit here.
  • Type consistency: UiItemSlot.SetItem(uint,uint) / .Clear() / .ItemId / .IconTexture / .EmptySprite / .Clicked; UiItemList.Cell / .GetItem(int) / .GetNumUIItems() / .AddItem(UiItemSlot) / .Flush(); IconComposer.Compose(IReadOnlyList<(byte[],int,int)>) / .GetIcon(uint,uint,uint); ItemRepository.EnrichItem(uint,uint,string,ItemType); ToolbarController.Bind(ImportedLayout, ItemRepository, Func<IReadOnlyList<ShortcutEntry>>, Func<uint,uint,uint,uint>, Action<uint>). These match across Tasks 5-9.
  • Known executor confirmations (grep-to-confirm, not placeholders): the exact name/signature of TextureCache's private RGBA upload (Task 5 Step 4); the EntitySpawn record location (Task 3 Step 1); the UiEvent/UiEventType member shape (Tasks 6/8); whether a guid-based use helper already exists near GameWindow.cs:11577 (Task 9 Step 3). Each step names the grep + the change.