merge: integrate main (D.2b paperdoll/inventory UI line) into the physics/collision branch

Brings the 134 D.2b UI commits onto the physics/collision development line so
main can fast-forward. Today's #149 (BSP-less static collision) + #150 (open
doors fully passable) + the full collision/streaming/dense-town-FPS arc meet
the paperdoll/inventory work.

# Conflicts:
#	docs/ISSUES.md
#	docs/architecture/retail-divergence-register.md
This commit is contained in:
Erik 2026-06-25 12:57:46 +02:00
commit 01594b4cfd
95 changed files with 17201 additions and 167 deletions

View file

@ -46,7 +46,7 @@ Copy this block when adding a new issue:
---
## #147 — Far-town (Arwic) collision broken at login: terrain barely grounds + city/perimeter walls never block
## #151 — Far-town (Arwic) collision broken at login: terrain barely grounds + city/perimeter walls never block
**Status:** 🟡 city/perimeter walls **FIXED + user-verified** (`9743537`, 2026-06-24); terrain-grounding sub-question OPEN but re-scoped LOW (likely not a real defect).
**Severity:** MEDIUM (walls were the HIGH part — fixed; terrain residual unverified)
@ -67,7 +67,7 @@ Copy this block when adding a new issue:
---
## #146 — Building/house-wall collision lost after portaling INTO a town (works on fresh login)
## #152 — Building/house-wall collision lost after portaling INTO a town (works on fresh login)
**Status:** DONE (`49d743f`, 2026-06-24) — root confirmed via the `bldOrigin` probe (Holtburg shell cached at the Arwic frame, ~5.5 km off) and fixed by re-basing the building cache on each landblock apply (`PhysicsDataCache.RemoveBuildingsForLandblock` + clear-then-repopulate in `ApplyLoadedTerrain`). Verified on the exact repro (Arwic login → Holtburg portal): `bldOrigin` now at the wall, channel returns `Collided`/`Slid`; suites green. (Move to Recently closed on next tidy.)
**Severity:** MEDIUM (clip through house/building walls after every portal-in; doors still block; terrain solid)
@ -88,7 +88,7 @@ Copy this block when adding a new issue:
---
## #145 — Portals only work once per session (can't run in, run out, re-enter)
## #153 — Portals only work once per session (can't run in, run out, re-enter)
**Status:** 🟡 PARTIALLY FIXED — REOPENED 2026-06-21 (a NARROWER residual). The cell-relative carried-anchor (Option B, Slices 13+7, `438bb68``403a338`) **fixes the cascade for a STREAMED-terrain arrival** (live-verified: `+Je` ran through ~10 far-town landblocks across 2 sessions, zero march). BUT a third live session found the cascade **RECURS when a teleport arrives onto a NOT-YET-STREAMED landblock NEAR AN EDGE.** Evidence (`launch5.log`): arrival `[snap] claim=0xC98C0028 pos=(113.666,190.259,22.010) branch=NO-LANDBLOCK (lbs=0) -> verbatim` (local Y=190.3, **1.7 m from the 192 boundary**) → cell marches `0xC98C(lbY 0x8C) → 0xC9FE(lbY 0xFE)` +2/tick while Z free-falls 22→19; the outbound wire then sends the marched cell `C9FE0031` with compensating garbage `localY=21684` → ACE `MOVEMENT SPEED` / `failed transition` → player stuck. Contrast same session: hub `0x00070133` VALIDATED→fine; far town `0x977B000C` arrived unstreamed but **mid-block** (Y=73.8) → Z free-fall only, NO march. **ROOT (hypothesis, NOT yet apparatus-confirmed):** outdoor teleport places immediately but the destination streams a few ticks later (`streaming: dungeon EXIT-expand -> (201,140)` logs just after placement); during that gap the resolve runs against an empty world, and at an edge the player crosses into the unstreamed neighbour where the carried anchor (which computes to `(0,0,0)` at the recentered center) gives no protection. Same root as the Z free-fall (`#135`/`#138` placed-but-unstreamed gap). **DO NOT guess-patch** (original #145 burned 5 attempts) — needs a capture + an anchor/guard diagnostic at the crossing FIRST. Likely fix shape: a streaming-gap HOLD (freeze the per-tick resolve — no march, no fall — until the player's landblock loads; the async equivalent of retail's synchronous load), which would fix both symptoms. Suites still green; `TeleportFarTownRunawayTests` covers the STREAMED case only. [Prior "fixed" banner was premature — 2 clean sessions wasn't enough for a streaming-timing bug.]
@ -181,7 +181,81 @@ Tests: `DungeonStreamingGateTests` (4), `TeleportArrivalRulesTests` (4). Registe
---
## #144 — Dungeon interiors still read too dim vs retail (torch-sparse stretches + per-vertex bake)
## #148 — Status-bar backpack icon should toggle the inventory window (stateful open/closed)
**Status:** OPEN
**Severity:** MEDIUM
**Filed:** 2026-06-22
The backpack icon in the status bar / toolbar should be **clickable to open/close the inventory
window** (toggle), and its art should reflect the inventory's state — a **closed** backpack when the
inventory is closed, an **open** backpack when it's open. Today the inventory opens via F12 only;
there's no clickable status-bar affordance and the icon isn't stateful. Wire the click → inventory
toggle (the window manager / `UiHost` RegisterWindow + the F12 toggle path) and swap the icon sprite
on the inventory window's visibility change. Investigate the retail status-bar backpack element +
its open/closed sprites when picked up.
---
## #147 — Inventory item-grid scrolling polish
**Status:** OPEN
**Severity:** LOW
**Filed:** 2026-06-22
The inventory contents-grid scrolling (gutter scrollbar `0x100001C7` bound to `UiItemList.Scroll`,
whole-row clip via `UiScrollable`) works but needs visual/UX polish — smoothness, thumb behavior,
wheel step, row-clip edges. Confirm the exact symptoms with the user when picked up. Filed from the
D.2b inventory visual gate.
---
## #146 — D.2b inventory capacity-bar visual polish
**Status:** OPEN
**Severity:** LOW
**Filed:** 2026-06-22
The per-side-bag / main-pack container **capacity fill bar** (faithful port of retail
`UIElement_UIItem::UpdateCapacityDisplay 0x004e16e0`, element `0x10000347`) shipped + is
visually confirmed (`src/AcDream.App/UI/UiItemSlot.cs` `CapacityFill`;
`InventoryController.SetCapacityBar`). User: "looks good, we need to polish it a bit, but lets do
that later." Get the specific polish list from the user when picked up. Candidate points (confirm
against retail):
- **Exact bar rect / anchor** — currently right-anchored flush (`X = Width barW`); the dat element
rect is `X=26 Y=1 W=5 H=30` (a 5px right margin). Flush was the visual-gate call; reconcile with
the dat once the desired look is pinned.
- **Fill direction** — currently bottom-up (assumed); the dat `m_eDirection` (property `0x6f`) isn't
read (cf. AP-50). Confirm bottom-up vs retail.
- **Closed-bag fill** — a closed side bag reads empty until opened (contents aren't indexed until
`ViewContents`). If retail shows real fill for closed bags, the per-container item counts must be
pre-loaded at login (the lazy-load question — also the container-switching "open verification").
See divergence register **AP-59**.
---
## #145 — Inventory window panels occluded by the full-window backdrop (importer ignores ZLevel)
**Status:** DONE (2026-06-21 · `45a5cc5` + continuation `417b137`) — VISUALLY CONFIRMED. The first fix (`45a5cc5`) folded `ZLevel` into `ZOrder` and put the backdrop behind the **ZLevel-0 top-level** panels, which made the **paperdoll** (its base root is ZLevel 0) render — so it *looked* done. But the **mounted backpack/3D-items panels** inherited their sub-window root's **ZLevel 1000** (via `ElementReader.Merge`'s zero-wins-base rule), so the `ReadOrder ZLevel·10000` fold sank them to ZOrder ≈ 10,000,000 — *behind* the backdrop (ZLevel 100 → ≈ 1,000,000). The Alphablend backdrop then **washed out** the backpack/3D-items captions + burden meter + item cells (only the bright paperdoll survived the wash). Surfaced once B-Controller populated those panels. Continuation fix `417b137`: the sub-window mount keeps each slot's **own** frame ZLevel (not the base root's 1000), so panels sit in front of the backdrop. Confirmed live (Burden 17% + vertical bar + "Contents of Backpack" + full item grid all render on the dark backdrop). Locked by `InventoryFrameImportProbe` (real-dat: each mounted panel's ZOrder > the backdrop's). DO-NOT-RETRY: a mounted sub-window slot must NOT inherit the base layout root's ZLevel. Per-slot paperdoll silhouettes (generic `UiDatElement` sprite — need `0x10000032` UiItemSlot + per-slot art) → Sub-phase C.
**Severity:** MEDIUM (blocks B-Grid visual acceptance — the gmInventoryUI nested panels render blank)
**Filed:** 2026-06-20
**Component:** ui — LayoutImporter / DatWidgetFactory z-order
**Description:** With `ACDREAM_RETAIL_UI=1`, F12 shows the `gmInventoryUI` (`0x21000023`) frame + title + the dark full-window backdrop sprite, but the three nested panels (paperdoll / backpack / 3D-items) render **blank**. The B-Grid sub-window mount IS attaching the panels' content — confirmed: the paperdoll base element `0x100001D4` (root of `0x21000024`) has 25+ equip-slot children — but the content is **occluded**.
**Root cause / status:** `DatWidgetFactory.Create` maps `ZOrder ← ReadOrder` only (`~:81`). In `0x21000023` the full-window backdrop element `0x100001D0` has `ReadOrder=4` (drawn AFTER the panels at `ReadOrder 1/2/3`), so it paints OVER them. Retail keeps the backdrop behind via **`ZLevel`** (backdrop `ZLevel=100` vs panels `ZLevel=0`); the importer ignores `ZLevel` because every vitals element was `ZLevel=0`, so it never mattered until now. Fix: factory `ZOrder` should honor `ZLevel` (higher `ZLevel` = further back) with `ReadOrder` as the tiebreaker — e.g. `ZOrder = ReadOrder ZLevel·K`. `ElementInfo` has no `ZLevel` field yet (add it + read in `ElementReader.ToInfo` + carry in `Merge`). **Regression risk:** touches z-order for every window — must verify chat (`0x21000006`) + toolbar (`0x21000016`) `ZLevel`s (vitals are all 0) and visually regression-test.
**Files:** `src/AcDream.App/UI/Layout/DatWidgetFactory.cs` (`e.ZOrder = (int)info.ReadOrder`, ~:81); `src/AcDream.App/UI/Layout/ElementReader.cs` (`ElementInfo` — add `ZLevel`; `ToInfo`; `Merge`); `src/AcDream.App/UI/Layout/LayoutImporter.cs`.
**Research:** `docs/superpowers/specs/2026-06-20-d2b-inventory-grid-mount-design.md` (B-Grid); dat dump of `0x21000023` (panels ReadOrder 1-3 / ZLevel 0; backdrop `0x100001D0` ReadOrder 4 / ZLevel 100).
**Acceptance:** F12 shows the nested paperdoll/backpack/3D-items panels over the backdrop (backpack burden meter + slot borders visible); vitals/chat/toolbar unchanged; full suite green.
---
## #154 — Dungeon interiors still read too dim vs retail (torch-sparse stretches + per-vertex bake)
**Status:** OPEN
**Severity:** MEDIUM (dungeons "better but not good enough" — user, 2026-06-20)
@ -238,6 +312,23 @@ Tests: `DungeonStreamingGateTests` (4), `TeleportArrivalRulesTests` (4). Registe
---
## #144 — Empty item-slot press+drag+release still emits a Click
**Status:** OPEN
**Severity:** LOW
**Filed:** 2026-06-20
**Component:** ui — D.2b drag-drop spine (B.1)
**Description:** Pressing an EMPTY `UiItemSlot`, moving the cursor >3 px (which makes `UiRoot.BeginDrag` cancel the drag because the empty cell's `GetDragPayload()` returns null), then releasing over the same cell still emits a `Click` (the press becomes a click on release, since no drag armed). This is the toolkit's general click semantics — every non-draggable widget (buttons included) fires Click after sub-gesture mouse movement; only an *armed* drag suppresses the click. Today it is a **no-op**: the toolbar's `Clicked` handler guards `if (Cell.ItemId != 0)`, so an empty-slot click uses nothing.
**Root cause / status:** Surfaced in the B.1 spine code review (Task 3). Whether retail fires nothing vs. a no-op click on an empty cell after a >3 px move is **unverified** — deliberately NOT "fixed" with a speculative `_dragCancelled` guard, because that would make empty item-slots behave differently from every other widget and would be guessing at retail behavior (CLAUDE.md forbids both). Becomes relevant only if a future panel (e.g. the inventory grid in Stream C) wires an empty-cell click handler that does NOT guard on occupancy. Action when touched: verify retail's empty-cell press+move+release behavior (cdb/decomp) before changing anything; if it must differ, it earns a divergence-register row.
**Files:** `src/AcDream.App/UI/UiRoot.cs` (`BeginDrag` cancel path, `OnMouseUp` Click emit), `src/AcDream.App/UI/UiItemSlot.cs` (`OnEvent` Click→Clicked).
**Research:** `docs/superpowers/specs/2026-06-20-d2b-drag-drop-spine-design.md` §6.
---
## #141 — Toolbar interactivity — selected-object display
**Status:** IN PROGRESS (D.5.3a health + name + flash — DONE & visually confirmed 2026-06-20; mana + stack slider still deferred). Renumbered from #140 on the 2026-06-20 main merge — A7 Fix D held #140 on main; this branch's commits/spec still reference #140.
@ -5538,6 +5629,71 @@ NOT pursued — both in frozen load-bearing subsystems, HIGH risk for ~15 fps at
extreme view already over target. SSOT: `docs/research/2026-06-23-dense-town-fps-attribution-report.md`.
Lessons: `memory/feedback_render_perf_measurement.md`.
---
## D.2b — Inventory window finish (Stage 1): scroll + frame + resize + 102 slots SHIPPED
**Closed:** 2026-06-21
**Component:** ui — D.2b inventory window (UiItemList scroll, UiNineSlicePanel frame, vertical resize)
The inventory window now matches retail's 2D presentation (minus the 3D paperdoll doll = Stage 2). Shipped `366af0c``1be7e65` (build + full suite green: Core.Net 334 / App 543 / UI 425 / Core 1530; spec/plan `docs/superpowers/{specs,plans}/2026-06-21-d2b-inventory-window-finish*.md`):
- **Scroll**`UiItemList` clip+scroll via the shared `UiScrollable` (whole-row clip; cells exempted from the per-frame anchor pass — that was the "grid escapes the window" root cause); gutter scrollbar `0x100001C7` bound like ChatWindowController; mouse-wheel.
- **Frame** — wrapped in the 8-piece bevel chrome (`UiNineSlicePanel`) like vitals/chat/toolbar.
- **Vertical resize** — bottom-edge drag, horizontal blocked (`ResizeX=false`); grid/sub-window/scrollbar/backdrop stretch (`Left|Top|Bottom`), paperdoll + side-bags pinned; scrollbar thumb reflects view/content. Expand-only Min=default..Max=560 (AP-54).
- **102-slot grid** — contents grid pads empty frames to the main-pack capacity (default 102, AP-53); side-bag column pads to 7 (AP-52).
**Visually confirmed** (scroll, frame, resize, 102 slots). **Remaining (handoff `docs/research/2026-06-21-d2b-inventory-finish-handoff.md`):** wrong empty-slot background art (inventory + equip slots — the OPEN issue above) + main-pack backpack icon (AP-51); Stage 2 = paperdoll `UiViewport` doll + per-slot equip silhouettes.
---
## D.2b-B — Inventory wire layer (B-Wire): player-property delivery + builders/parsers SHIPPED
**Closed:** 2026-06-21
**Component:** net/ui — D.2b inventory wire (player props + inventory GameMessages/GameActions)
The burden bar now reads the server's wire `EncumbranceVal` (PropertyInt 5) instead of the client-side sum. **Root cause was delivery, not binding** — the binding already read `Properties.Ints[5]`, but the value never arrived: login PD parsed the player's int table then dropped it; live `PrivateUpdatePropertyInt 0x02CD` was unparsed; `ObjectTableWiring` gated all non-UiEffects ints out. Fixes: `ClientObjectTable.UpsertProperties` (create-if-absent) + the PD handler upserts the player's `PropertyBundle`; new `PrivateUpdatePropertyInt 0x02CD` parser + WorldSession dispatch + player-int route; loosened the int-apply gate to apply ALL ints; `InventoryController.Concerns` refreshes on the player's own object (C1d). Plus latent-bug fixes (`0x0022` 4th field `containerType`; `0x00A0` `weenieError`), new builders (`DropItem 0x001B`, `GetAndWieldItem 0x001A`, `NoLongerViewingContents 0x0195`) + `Send*` wrappers, new parsers (`ViewContents 0x0196`, `SetStackSize 0x0197`, `InventoryRemoveObject 0x0024`) + GameEvent registration. Spec/plan `docs/superpowers/{specs,plans}/2026-06-21-d2b-inventory-wire*.md`. Commits `b56087b``7badecf` (build + full suite green: Core.Net 334 / App 534 / UI 425 / Core 1530; Opus phase-boundary review APPROVED with byte-for-byte ACE verification). AP-48/AP-49 reworded to fallback-only (confirm + delete at the visual gate). **Pending the burden-bar visual gate** (does the live bar match ACE + update on pick-up/drop?). Remaining D.2b inventory: contents-grid scroll polish (below), B-Drag (inventory drag SOURCE + `SourceKind==Inventory`), Sub-phase C (paperdoll).
---
## D.2b-B — Inventory controller (B-Controller): grid population + burden meter + captions SHIPPED
**Closed:** 2026-06-21
**Component:** ui — D.2b inventory window (gmInventoryUI 0x21000023)
`InventoryController.Bind` wired into the existing inventory-init block in `GameWindow.cs` (commit `03fbf44`). The controller populates the "Contents of Backpack" grid (6 cols × 32 px) and the pack-selector strip from `ClientObjectTable`, drives the vertical burden meter via `BurdenMath.EncumbranceCapacity/LoadRatio/LoadToFill`, and attaches "Burden" + "Contents of Backpack" + `%` captions. Four divergence rows added: AP-48 (client-side burden sum), AP-49 (aug capacity unwired), AP-50 (meter direction from geometry), AP-51 (main-pack icon placeholder). Divergence register + docs commit: `docs(D.2b-B)`.
**VISUALLY CONFIRMED 2026-06-21.** At the visual gate the controller's *logic* was correct (live diagnostics: 36 items + 1 side bag bound, burden Str 290 → 17%, captions' `DrawStringDat` called, meter sprites resolved), but two RENDER bugs hid the result; both fixed (`417b137`): (1) backdrop wash-out — the #145 continuation above; (2) captions — the caption elements resolve to `UiText`, and driving a nested *child* `UiText` didn't paint, so `AttachCaption` now drives the host `UiText` directly. After the fixes: dark backdrop behind, "Burden 17%" + vertical bar + side-bag + "Contents of Backpack" + full item grid all render. **Remaining open sub-phases:** B-Wire (parse `EncumbranceVal`/PropertyInt 5 from the wire to retire AP-48), B-Drag (drag-from-inventory SourceKind branch closes B.2), Sub-phase C (paperdoll equip-slot `UiItemSlot` registration), + the contents-grid scroll polish (next issue).
---
## Inventory "Contents of Backpack" grid overflows (no scroll)
**Status:** DONE (2026-06-21 · D.2b inventory finish Stage 1, `366af0c``1be7e65`) — the grid clips to its panel + scrolls via the gutter scrollbar `0x100001C7` (`UiScrollable` + whole-row clip; cells exempted from the anchor pass), pads to the full 102-slot main-pack capacity, and the window is vertically resizable (bottom edge). Visually confirmed.
**Component:** ui — D.2b inventory (gm3DItemsUI grid `0x100001C6`)
**Description:** `InventoryController` populates the 6-col contents grid with ALL of the player's loose pack items via `UiItemList` grid mode, so a pack with >18 items (6 cols × 3 visible rows in the 192×96 panel) overflows BELOW the panel/frame ("part of the inventory hanging off the window"). Retail scrolls the grid via the side gutter scrollbar (`0x100001C7`). Fix: wire the gutter `UiScrollbar` to the grid + clip the grid to the panel (scroll the overflow). Spec listed scrolling as out-of-scope for B-Controller (`docs/superpowers/specs/2026-06-21-d2b-inventory-controller-design.md` §10), so this is a deliberate follow-up, not a regression.
---
## Inventory + equipment slots show the wrong empty-slot background art
**Status:** INVENTORY PORTION FIXED + VISUALLY CONFIRMED 2026-06-22 — contents grid + side-bag + main-pack empty art dat-resolved (`ItemListCellTemplate`, commits `b862640``22d9231`; user-confirmed the container column reads correct). STILL OPEN: paperdoll equip-slot silhouettes (Sub-phase C) + main-pack backpack icon (AP-51) + the selected-container triangle/selection-square (container-switching sub-phase, AP-56).
**Filed:** 2026-06-21
**Component:** ui — D.2b inventory (UiItemSlot empty sprite + paperdoll equip slots)
**Description:** User-flagged at the Stage-1 visual gate: the empty cells in the contents grid + side-bag column use `UiItemSlot.EmptySprite = 0x060074CF` (the TOOLBAR empty-slot border) — the wrong art for inventory pack slots. The paperdoll equipment slots render a generic blue `UiDatElement` border, not per-slot equip silhouettes. Both look wrong vs retail.
**Root cause / status:** `UiItemSlot.EmptySprite` is a single hardcoded default (`0x060074CF`) shared by all empty cells. The inventory/side-bag pack slots need the gm3DItemsUI/gmBackpackUI cell-template (`0x21000037`) empty-state background instead. The equip slots need the `0x10000032` `UIElement_UIItem` registration + per-slot silhouette art (Sub-phase C).
**Resolution (inventory portion, 2026-06-22):** Ported retail's `UIElement_ItemList::InternalCreateItem` (`0x004e3570`) resolver into `ItemListCellTemplate.ResolveEmptySprite` — reads attribute `0x1000000e` off each list element → catalog `0x21000037` prototype's `ItemSlot_Empty`. **Correction to the original diagnosis:** `0x060074CF` is the *generic shared* item-slot empty (the `ItemSlot_Empty` of many catalog prototypes), not "the toolbar's"; `0x21000037` is a catalog of ~50 per-slot-kind empties — so it was never a one-constant swap. Pinned per-list (real-dat test): contents `0x100001C6``0x06004D20`; side-bag `0x100001CA` + main-pack `0x100001C9``0x06000F6E` (the inner dark slot, resolved through `BaseElement` inheritance). **Visual-gate correction (2026-06-22):** the first cut's frame-first heuristic grabbed the container prototype's DirectState child `0x06005D9C` — which is the open/**selected**-container TRIANGLE indicator, not a background — and stamped it onto every empty container cell. Fixed: resolve the inner `ItemSlot_Empty` through inheritance (`FindIconEmpty`); the triangle + green/yellow selection square is deferred to the container-switching sub-phase (AP-56). `UiItemList.CellEmptySprite` carries it; `InventoryController` + `GameWindow` wire it. Divergence AP-55 (toolbar still hardcoded) + AP-56 (selected-container indicator deferred). Spec/plan `docs/superpowers/{specs,plans}/2026-06-22-d2b-empty-slot-art*.md`.
**Files:** `src/AcDream.App/UI/UiItemSlot.cs` (`EmptySprite`); `src/AcDream.App/UI/Layout/InventoryController.cs` (set the inventory pack-slot empty sprite on the cells it creates); paperdoll equip slots → Sub-phase C.
**Research:** `.layout-dumps/uiitem-0x21000037.txt` (cell template states); `docs/research/2026-06-16-equipment-paperdoll-deep-dive.md`. Also pending: the main-pack backpack icon (AP-51).
**Acceptance:** empty inventory + side-bag cells show the retail pack-slot frame; equip slots show per-slot silhouettes.
---
## #113 — Phantom staircase: REOPENED 2026-06-11, folded into the HOLISTIC BUILDING-RENDER PORT
**Status:** REOPENED — root cause #2 found (drawing-BSP-orphaned no-draw polys: