Commit graph

2171 commits

Author SHA1 Message Date
Erik
4904ff4e21 docs(issues): #145 DONE — ZLevel fix renders the inventory panels
Occlusion fixed (45a5cc5); paperdoll equip-slot positions visually
confirmed. Remaining gaps are next-sub-step content/art, not occlusion:
backpack/3D-items population → B-Controller; per-slot paperdoll
silhouettes → Sub-phase C.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 07:36:55 +02:00
Erik
b9445f53fe docs: handoff for #138 (entity re-delivery after teleport) — next session
Full orientation for a fresh session to fix #138: confirmed root (server
objects unloaded on teleport-IN, not restored on return; ACE re-broadcast
unreliable), DO-NOT-RETRY table (cache + render-cull eliminated), the
ClientObjectTable re-hydrate fix direction with file:line pointers, the
launch/probe/account setup, and the gotchas (re-broadcast latency, stale
sessions, don't-kill-clients, entity.Id != ServerGuid).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 07:29:00 +02:00
Erik
c0b2cf2f7b docs(issues): #138 re-scoped — it's entity RE-DELIVERY across a teleport, not render-cull/cache
Deep dive (this session) eliminated the 2026-06-20 hypotheses for #138
(server objects + own avatar not showing after a teleport-out):
- NOT the Tier-1 classification cache: re-created live entities get a fresh
  monotonic Id (_liveEntityIdCounter++), so the cache (keyed on Id) is always
  a miss for them. (Side-finding: the cache has a real demote-vs-unload
  invalidation asymmetry — RemoveLandblock doesn't fire _onLandblockUnloaded
  while RemoveEntitiesFromLandblock does — but it's NOT the #138 cause.)
- The render path is fine when entities are present (login: [dyn] dyn=54
  drawn=33; the dynamics partition + DrawDynamicsLast draw them).
- The actual cause: re-delivery is unreliable. notan/+Je walk-around run after
  teleport-out: live:spawn doors=0, [ent]+ door appends=0, [ent-flat] server=1
  — the server delivered ZERO Holtburg objects on return; they never reach
  acdream. acdream unloads them on teleport-IN (the collapse) and nothing
  restores them; ACE doesn't reliably re-broadcast. "Other clients see +Je"
  confirms it's acdream's local world, not server state.

Fix direction (next session): re-hydrate GpuWorldState from the retained
ClientObjectTable on AddLandblock instead of depending on an ACE re-broadcast
(or treat in-range server objects as persistent across the collapse). Entity-
lifecycle/protocol change, best started fresh.

Diagnostic scaffolding (probes + the unrelated cache-asymmetry fix) reverted;
tree is back at the green #145 state (a15bd3b).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 07:24:01 +02:00
Erik
45a5cc5bb6 fix(ui): #145 — importer honors ZLevel so the backdrop sits behind panels
The factory mapped ZOrder from ReadOrder only, so the gmInventoryUI
full-window backdrop (0x100001D0, ReadOrder 4) painted over the nested
panels (ReadOrder 1-3). Wire ElementDesc.ZLevel through ElementInfo /
ToInfo / Merge and fold it into ZOrder = ReadOrder - ZLevel*10000 (higher
ZLevel = further back, ReadOrder the within-layer tiebreaker). Vitals
(all ZLevel 0) are unchanged; chat (ZLevel 900) + toolbar (1,2) shift to
their dat layering — verify visually.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 07:22:26 +02:00
Erik
d81ea11a31 docs(issues): #145 — inventory panels occluded by full-window backdrop
B-Grid sub-window mount works (paperdoll base 0x100001D4 has 25+ equip-slot
children, attached), but the gmInventoryUI panels render blank: the
importer maps ZOrder from ReadOrder only, so the full-window backdrop
(0x100001D0, ReadOrder 4) paints over the panels (ReadOrder 1-3). Retail
keeps it behind via ZLevel (backdrop 100 vs panels 0), which the importer
ignores (vitals were all ZLevel 0). Fix scoped in the issue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:58:11 +02:00
Erik
7d971a2b3e feat(ui): D.2b-B — F12 shows the real gmInventoryUI (0x21000023)
Swap the Sub-phase A placeholder UiNineSlicePanel for LayoutImporter.Import
(0x21000023). The sub-window mount nests the paperdoll/backpack/3D-items
panels; the window starts hidden, registered under WindowNames.Inventory,
toggled by F12 via the window manager (unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:37:01 +02:00
Erik
85098f535d feat(ui): D.2b-B — sub-window mount (inheritor attaches base subtree)
LayoutImporter.Resolve now captures the resolved base element's children
and, for a pure-container leaf (no own children + no own media) inheriting
from a base WITH content, attaches that subtree. This pulls the nested
gmInventoryUI panels' content (paperdoll/backpack/3D-items) in through the
existing BaseElement+BaseLayoutId path. ShouldMountBaseChildren is a pure,
unit-tested predicate; it's inert for media-bearing inheritors and childless
style prototypes, so vitals/chat/toolbar are unaffected (existing importer
tests still green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:36:20 +02:00
Erik
4fd4b09f3f feat(ui): D.2b-B — UiItemList N-cell grid mode
Columns + CellWidth/CellHeight + a row-major CellOffset/LayoutCells pass.
CellWidth<=0 keeps the single-cell fill mode (toolbar slot sizes to the
list — unchanged); CellWidth>0 tiles cells in a grid. Layout runs on
AddItem and per-frame in OnDraw so a list resize reflows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:34:30 +02:00
Erik
132bf36daa docs(D.2b-B): B-Grid implementation plan
4-task TDD plan: UiItemList grid mode (CellOffset/Columns/cell pitch),
the LayoutImporter.Resolve sub-window mount (ShouldMountBaseChildren +
attach base subtree), GameWindow swap of the placeholder for the real
Import(0x21000023), then full-suite regression guard + visual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:32:37 +02:00
Erik
aa60615912 docs(D.2b-B): B-Grid design spec — inventory sub-window mount + grid mode
First step of Sub-phase B (inventory window). Dumped LayoutDesc 0x21000023:
the three nested panels (paperdoll/backpack/3D-items) are Type-0 pure-
container leaves that nest via the EXISTING BaseElement+BaseLayoutId
inheritance path (BaseLayoutId → 0x21000024/22/21), NOT game-class Types as
the research agent claimed. ElementReader.Merge drops base children, so they
import empty today. Mount = a surgical ~4-line LayoutImporter.Resolve change:
a childless, media-less inheritor attaches its base's resolved subtree
(predicate ShouldMountBaseChildren, unit-testable; inert for media-bearing
inheritors + childless style prototypes, so vitals/chat/toolbar are
unaffected). Plus UiItemList N-cell grid mode (single-cell toolbar preserved).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 22:25:50 +02:00
Erik
a15bd3b56d fix(streaming): #145 — teleport re-use via server-authoritative placement
Portals only worked once per session: teleporting OUT of a dungeon
mis-rooted the player into the SOURCE dungeon's coordinate frame, so every
move was sent dungeon-framed and ACE rejected it ("failed transition") —
the player couldn't move, never reached a portal, and the world wouldn't
re-render (only skybox).

Root cause: acdream's streaming-relative frame recenters on teleport, but
resident physics landblocks keep their load-time world-offset. After
recentering onto the outdoor destination, the collapsed source dungeon
(offset 0,0 as the prior center) and the destination (offset 0,0 as the
new center) overlap, and the Z-agnostic outdoor cell-snap returns the
dungeon for both the arrival placement and every per-frame resolve.

Fix (server-authoritative teleport placement):
- Drop the stale source center landblock from physics at the teleport
  recenter (GameWindow.OnLivePositionUpdated) so the resolve falls through
  to the server position (Resolve NO-LANDBLOCK verbatim) until the
  destination streams in.
- Place outdoor teleports immediately (TeleportArrivalRules) — holding is
  futile because streaming does not progress during a PortalSpace hold.
- Clear a dangling CellGraph.CurrCell when its landblock is removed
  (PhysicsEngine.RemoveLandblock) — otherwise the dungeon-streaming gate
  keeps streaming collapsed onto the gone dungeon (only skybox renders).

Keeps DungeonStreamingGate (gate suppression during the hold). Indoor
(dungeon-entry) placement is unchanged (cell-keyed, IsSpawnCellReady).

User-verified: in->out->re-enter works repeatedly, no ACE errors, world
renders. Remaining facets (server objects + own avatar not rendering after
a teleport-out) are entity render/lifecycle — split to #138.

Registers AP-36 + AD-2 updated. New: DungeonStreamingGate (+4 tests),
TeleportArrivalRules (+4 tests). Build + 2727 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:38:00 +02:00
Erik
e3152ade9a docs(register): D.2b-A — extend IA-12 to cite the window manager
The UiRoot window manager (RegisterWindow/Show/Hide/Toggle/BringToFront)
is more of the same IA-12 toolkit-reimplementation of keystone semantics,
so per the dedup convention it joins IA-12's Where as a secondary site
rather than getting a redundant row. F12 inventory toggle is retail-
faithful (no divergence).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:33:04 +02:00
Erik
882b4dd5d3 feat(ui): D.2b-A — F12 toggles placeholder inventory window
UiHost RegisterWindow/ToggleWindow forwarders to UiRoot. A default-hidden
placeholder inventory window mounts in the RetailUi block + registers as
WindowNames.Inventory. OnInputAction handles the existing F12-bound
ToggleInventoryPanel action -> _uiHost.ToggleWindow (no-op when retail UI
off; gated by WantsKeyboard so it can't fire while typing in chat).

Placeholder is throwaway scaffolding; Sub-phase B swaps in the real
gmInventoryUI (0x21000023) under the same registry name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:32:17 +02:00
Erik
036db8b8ab feat(ui): D.2b-A — BringToFront + raise on show/click
BringToFront sets a window's ZOrder one past the max among its peers.
ShowWindow now raises on open; OnMouseDown raises any pressed top-level
window (retail-faithful stacking). Existing drag/resize tests unaffected
(raise only touches ZOrder, not geometry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:31:29 +02:00
Erik
6409038576 feat(ui): D.2b-A — UiRoot named-window registry (Show/Hide/Toggle)
A Dictionary<string,UiElement> registry on UiRoot with RegisterWindow +
Show/Hide/Toggle. Show/Hide flip UiElement.Visible (already gates
Draw/Tick/HitTest); Toggle returns the new visibility; unknown names are
no-ops. WindowNames.Inventory const shared by mount/registry/toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:30:22 +02:00
Erik
8457bf0006 docs(D.2b-A): window manager implementation plan
4-task TDD plan for Sub-phase A: UiRoot named-window registry
(Show/Hide/Toggle/BringToFront) + raise-on-click + F12 wiring of the
existing ToggleInventoryPanel action to a throwaway placeholder inventory
window that Sub-phase B replaces. Divergence handled by extending IA-12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:28:50 +02:00
Erik
8f30585cb0 docs(D.2b-A): window manager + F12 inventory toggle design spec
Sub-phase A of the window-manager → inventory → paperdoll arc. A named-
window registry on UiRoot (Show/Hide/Toggle/BringToFront) + raise-on-click
+ wiring the EXISTING InputAction.ToggleInventoryPanel (already F12-bound,
retail-faithful) through OnInputAction, toggling a throwaway placeholder
inventory window that Sub-phase B replaces with the real gmInventoryUI.

Cross-check correction vs the handoff: retail's inventory key is F12, not
I (I = Laugh emote per retail-default.keymap.txt:246). Reuses the existing
action, so no new keybind — rebindable via the Settings panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:21:29 +02:00
Erik
a391b86a2e docs(handoff): window manager → inventory window → paperdoll (next D.2b arc)
Frames the next core-panels work for a fresh session, after B.1 (drag spine) +
B.2 (toolbar reorder/remove + wire) + toolbar collapse merged this session.
Three sub-phases in order: (A) minimal window manager (open/close + I-key toggle;
UiHost has no open/close API today, windows are always-on); (B) inventory window
(Stream C, synthesis §4 — UiItemList grid + sub-window mount + wire gaps +
InventoryController; the drag SOURCE that closes B.2's drag-from-inventory via a
SourceKind==Inventory branch in ToolbarController.HandleDropRelease); (C) paperdoll
UiViewport (Type 0xD) + Core→App IUiViewportRenderer seam, heaviest, last. Spell
bar deferred. Reuse the shipped spine/ShortcutStore/wire. Paste-able new-session
prompt + MEMORY index line inside.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 20:38:41 +02:00
Erik
abbd97bc7d Merge branch 'claude/hopeful-maxwell-214a12'
# Conflicts:
#	docs/ISSUES.md
2026-06-20 20:32:21 +02:00
Erik
14443e5c27 fix(ui): D.2b — collapsed toolbar left black pillars + draggable below the bar
Two bugs from the visual gate, same dump-confirmed cause: row 2 is an 11-element
band (left edge-piece 0x100006B6 + 9 slots 0x100006B7..BF + right edge-piece
0x100006C0, all at content-y 90), but SecondRow hid only the 9 slots — so the two
edge-pieces kept drawing as black pillars below the collapsed bar. And toolbarRoot
(ClickThrough=false, full height) still caught clicks below the collapsed frame →
walked up to the Draggable frame → phantom window-move.

Fix: SecondRow now hides the full band (B6..C0); toolbarRoot.ClickThrough=true so
its children (slots/indicators) still get hits children-first but its empty/below
regions fall through (no phantom drag) — same pattern as the chat content panel.
The slots are edge-flag 0 = Left|Top fixed, so nothing reflows. Build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:52:49 +02:00
Erik
89a2a2857a test(ui): D.2b — cover ResizeRect maxH clamp (Bottom + Top edges)
Spec §6 test 3 — the maxH clamp is the load-bearing path for the toolbar collapse; the implementer updated the existing ResizeRect tests' signatures but didn't add the clamp-verification cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:37:33 +02:00
Erik
f74e017509 feat(ui): D.2b — toolbar collapse-to-one-row (bottom-edge snap resize hides/shows row 2)
UiElement.MaxHeight + ResizableEdges mask; UiCollapsibleFrame snaps height to the nearer
of {collapsed,expanded} and toggles row-2 visibility; GameWindow computes the two heights
from the layout + top-anchors the content. Amends IA-17. UiNineSlicePanel unsealed to
allow subclassing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:33:35 +02:00
Erik
f6a576af8e docs: file #145 (portals work once/session) + session handoff (next: #145 then #144)
Indoor lighting DONE this session (#142/#143 closed). Per user: do NOT merge to
main; start fresh next session. NEXT-session order set in the handoff:
1. #145 — portals only work once per session (run in/out/re-enter repeatedly).
   Machinery: 0xF751 PlayerTeleport -> PortalSpace -> TeleportArrivalController ->
   streaming collapse/expand. Likely overlaps #138. Instrument a 2nd teleport first.
2. #144 — dungeon interiors still too dim vs retail (cdb side-by-side first).

Handoff: docs/research/2026-06-20-indoor-lighting-done-next-portal-reuse-handoff.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:31:47 +02:00
Erik
e58be3f030 docs(D.2b): toolbar collapse-to-one-row design spec
User request: the toolbar frame resizes vertically between one row (row 2
hidden, minimum) and two rows (shown), SNAPPING between the two stops; default
expanded. Small toolkit feature: UiElement.MaxHeight + a ResizableEdges mask
(bottom-edge-only) + a UiCollapsibleFrame (snaps height to the nearer stop and
ties row-2 visibility to it in OnTick) + the GameWindow mount (compute the two
heights from the layout, top-anchor the content so row 1 never reflows). Retail's
real mechanism is keystone.dll (no decomp) + the dat stacks both rows always —
so this is a toolkit UX from the user's retail observation; amends IA-17.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:25:46 +02:00
Erik
653e7f380f docs(issues): close #142 + #143 (indoor lighting); file #144 (dungeon still too dim)
#142 + #143 resolved this session — interiors + the meeting-hall portal now match
retail (user-confirmed). The #142 diagnosed cause (per-frame sun/ambient regime)
was a red herring; the real bug was the EnvCellRenderer landblock-key lookup
(0d8b827) that starved every interior wall of point lights. #143's portal light
rides the weenie-light path + the dynamic D3D 1/d attenuation (57c2ab7).

#144: dungeons improved (torch cells light up now) but torch-sparse stretches +
overall brightness still trail retail. Needs a side-by-side cdb capture of
retail's dungeon (active lights + ambient) — candidates: per-vertex bake under-lit
on low-poly walls, 0.2 sealed ambient too dark, or retail leans harder on dynamics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:25:22 +02:00
Erik
57c2ab735d fix(lighting): #143 — dynamic lights use retail's D3D 1/d attenuation (portal + viewer spread)
The portal swirl's magenta light (and the viewer fill) read as a tight,
concentrated pool vs retail's soft, room-wide tint. Cause: acdream applied its
STATIC dat-bake falloff (1/d^3 distance-cube + range x1.3) to ALL point lights,
including dynamic ones. Retail draws dynamic lights through the D3D hardware
path (config_hardware_light 0x0059ad30): a point light gets Attenuation1=1 =>
att = 1/d (inverse-linear), plain Lambert, range x1.5 (rangeAdjust 0x00820cc4).

Split the two paths by a per-light IsDynamic flag:
- LightSource.IsDynamic; packed into GlobalLight.coneAngleEtc.y (binding=4).
- LightInfoLoader.Load(isDynamic) => range x1.5 + flag (server-object/portal
  lights via the live spawn path); dat-static lights keep x1.3 (default).
- Viewer fill + weenie/portal lights = dynamic; dat torches = static.
- mesh_modern.vert pointContribution: dynamic branch = 1/d att, plain Lambert,
  hard cutoff, no per-light cap (D3D accumulates then saturates via the existing
  min(pointAcc,1)); static branch = the unchanged wrap/norm bake.

This is the portal half of #143 (the magenta light itself now registers + reaches
the walls via the prior weenie-light + landblock-key fix). Refines AP-35: point
lights now split static-bake (1/d^3) vs dynamic-hardware (1/d) by path.

Verified: portal light now range=9 (6x1.5), magenta spreads softly; shader
compiles clean; static torches unchanged (range 5.2/6.5/7.8). User-confirmed the
portal matches retail and the torch-lit interior did not over-brighten.
Core lighting 44/44, App 476 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:14:57 +02:00
Erik
049a099123 docs(register): correct TS live-row count (31 → 32, pre-existing undercount)
The final B.2 review found the TS header undercounted: there are 32 contiguous
rows (TS-1..TS-32) but the header read "31". The base already undercounted (said
"32" with 33 rows TS-1..TS-33); deleting TS-33 with a literal 32→31 decrement
preserved the off-by-one. Set the header to the verified actual count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:59:14 +02:00
Erik
0d8b827721 fix(lighting): indoor interiors now lit — landblock-key bug + viewer light + weenie fixtures
The whole "indoor interiors read dark/flat vs retail" saga was ONE root-cause
bug: EnvCellRenderer.GetCellLightSet derived the landblock key as
`cellId & 0xFFFF0000` (0xXXYY0000), but landblocks are keyed by the streaming
id 0xXXYYFFFF. The lookup missed for EVERY cell, so SelectForObject never ran
and every EnvCell wall received ZERO point lights — torches, lanterns, the
viewer light, all of it. Confirmed by a [cell-light] probe: inBounds=False
selected=0 across 1M+ cell draws; after the fix inBounds=True selected=3-4.
User-confirmed the interiors now look like retail.

Three faithful additions that were blocked by the key bug (and only show now):
- Viewer light (LightManager.UpdateViewerLight): retail's SmartBox::set_viewer
  (0x00452c40) adds a white fill light at the player every frame via
  add_dynamic_light — the dominant interior fill (no sun indoors). acdream had
  NO dynamic lights at all. Params from the cdb capture: intensity 2.25,
  falloff 10, white, offset (0,0,2). Indoor-only via the AP-43 gate.
- Weenie fixture lights (OnLiveEntitySpawnedLocked): server-spawned lanterns/
  braziers carry Setup.Lights but the dat-static registration never saw
  CreateObject entities. Register on spawn; unregister on despawn
  (UnregisterOwner made unconditional). Register row AP-44.
- IndoorObjectReceivesTorches now excludes the 0xFFFF landblock marker (it is
  not an EnvCell) — fixes WbDrawDispatcherIndoorFlagTests.LandblockId_OutdoorFlag0
  (a #142 verification miss).

Divergence register: AP-44 (weenie light spawn-position, no movement tracking),
AP-47 (acdream's 128-light/camera-independent cap keeps interiors always-lit vs
retail's 40-nearest-to-player budget that pops in on approach — intentional,
user-preferred).

Investigation: docs/research/2026-06-20-indoor-torch-lantern-lighting-investigation.md

Core 1505 / App 476 green. Visual gate: user-confirmed "looks like retail now."

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:51:55 +02:00
Erik
ff3592ec25 refactor(core): D.5.3/B.2 — move ShortcutStore to Core.Items (decouple Load from the wire type)
Matches the ClientObjectTable model-placement convention; Load now takes (slot,objGuid) pairs so
the store has no Core.Net dependency. + self-drop wire-count assert + comment fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:50:33 +02:00
Erik
250f7827be feat(ui): D.5.3/B.2 — toolbar reorder/remove via ShortcutStore + wire + green-cross overlay
ToolbarController is now the LIVE drag handler:
- OnDragLift: removes the source slot from ShortcutStore, sends
  RemoveShortcut (0x019D) wire immediately (retail remove-on-lift model).
- HandleDropRelease: evicts occupant from target (RemoveShortcut),
  places dragged item (AddShortcut 0x019C), bumps evicted item into
  the vacated source slot if empty (swap path); off-bar release leaves
  the lift's removal standing.
- Populate() now lazy-loads ShortcutStore from the PD shortcut list on
  first call; store is authoritative thereafter.
- IsShortcutGuid() uses the store (O(18)) when loaded, falls back to
  scanning _shortcuts() in the pre-PD window.
- All 18 slot cells now get DragAcceptSprite=0x060011FA (green cross,
  distinct from the inventory ring 0x060011F9).
- GameWindow.Bind wired: sendAddShortcut + sendRemoveShortcut lambdas
  forwarded to _liveSession?.Send*().
- Divergence register: AP-47 Divergence+Risk cells updated (opacity
  corrected, underlay-backing framing improved). TS-33 row deleted
  (stopgap retired). Section header 32→31 rows.
- Tests: HandleDropRelease_isInertStub removed; 5 new B.2 tests added
  (lift removes + sends, swap sequence, empty-target place, self-drop
  re-adds, green-cross sprite). 24/24 ToolbarController tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:33:23 +02:00
Erik
41bb70c11a feat(ui): D.5.3/B.2 — spine: drag-lift hook + ghost snapshot (full opacity) + drop-on-hit-only
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:22:27 +02:00
Erik
a497cc631e test(core): D.5.3/B.2 — move ShortcutStoreTests to Core.Net.Tests (Rule 6) + cosmetic cleanups
- Move ShortcutStoreTests from AcDream.Core.Tests to AcDream.Core.Net.Tests (Rule 6:
  tests live in the project matching the layer under test; ShortcutStore is Core.Net)
- Replace fully-qualified System.Buffers.Binary.BinaryPrimitives. with BinaryPrimitives.
  in the two new BuildAddShortcut tests (file already has `using System.Buffers.Binary`)
- Add `using System;` to ShortcutStore.cs; change System.Array.Clear → Array.Clear
  (matches sibling file style, no behavior change)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:18:35 +02:00
Erik
745a92bbae feat(core): D.5.3/B.2 — ShortcutStore + AddShortcut/RemoveShortcut wire + senders
ShortcutStore lands in AcDream.Core.Net.Items (not AcDream.Core.Items) because
it depends on PlayerDescriptionParser.ShortcutEntry; placing it in AcDream.Core
would create a circular dependency (Core.Net already references Core). The test
lives in AcDream.Core.Tests which gets Core.Net transitively via App.

BuildAddShortcut signature corrected from the old (seq, slotIndex, objectType,
targetId) 4×u32 layout to the retail ShortCutData wire format confirmed in the
action-bar deep-dive: Index(u32), ObjectId(u32), SpellId(u16), Layer(u16).
The old BuildAddShortcut_ThreeFields test is replaced by two new tests that
verify both item and spell shortcut packing.

WorldSession gains SendAddShortcut / SendRemoveShortcut following the
SendChangeCombatMode sender pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:09:58 +02:00
Erik
7193bed309 docs(D.5.3/B.2): toolbar shortcut drag implementation plan
Bite-sized TDD plan, three slices: (1) Core ShortcutStore + the AddShortcut
0x019C/RemoveShortcut 0x019D wire (rename BuildAddShortcut to the real
Index/ObjectId/SpellId/Layer fields; SendAddShortcut/SendRemoveShortcut); (2)
spine extensions (OnDragLift hook; ghost snapshotted at BeginDrag at full
opacity; FinishDrag delivers a drop only on a real hit); (3) ToolbarController
as the live handler (store-driven Populate w/ lazy-load; green-cross FA overlay;
OnDragLift removes + HandleDropRelease places + bumps displaced→source; wire
actions injected from GameWindow). Amends AP-47, deletes TS-33. Spec + plan
preapproved; executing subagent-driven next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:05:02 +02:00
Erik
19f1b8b614 docs(D.5.3/B.2): toolbar shortcut drag interactivity design spec
From the user's visual-gate feedback + a retail decomp trace this session.
Retail's model is remove-on-lift / place-on-drop / no-restore (confirmed:
RecvNotice_ItemListBeginDrag 0x004bd930 → RemoveShortcut 0x004bd450 at lift;
HandleDropRelease 0x004be7c0 places + bumps displaced → source; off-bar drop
leaves it removed). This unifies the user's points 3/4/5 into one mechanism.

Verified the drop sprite against client_portal.dat: 0x060011F9 = green RING
(inventory), 0x060011FA = green CROSS (toolbar) — user was right.

Scope (toolbar-internal; drag-from-inventory is Stream C): spine extensions
(lift hook on IItemListDragHandler; ghost snapshotted at BeginDrag at full
opacity so it survives the source emptying; FinishDrag delivers a drop only on
a real hit, off-bar = nothing); a mutable 18-slot ShortcutStore; ToolbarController
as the live handler (OnDragLift removes; HandleDropRelease places + bumps); the
AddShortcut 0x019C / RemoveShortcut 0x019D wire (fix the BuildAddShortcut param
names; SendAddShortcut/SendRemoveShortcut on WorldSession); green-cross overlay.
Amends AP-47 (ghost now full opacity), retires TS-33 (real wire replaces the stub).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:57:31 +02:00
Erik
acdefc2e21 fix(ui): D.5.3/B.1 — dragging a toolbar item moved the window instead of the item
Found at visual verification: an occupied UiItemSlot sits inside the Draggable
toolbar frame (UiNineSlicePanel.Draggable=true), so UiRoot.OnMouseDown's FindWindow
returned the frame and the window-move branch won — press+drag on a slot moved the
whole bar instead of picking up the item. The slot wasn't CapturesPointerDrag (that
path is for self-driven text-selection and suppresses the BeginDrag promotion), and
UiRoot had no path for "a drag-source inside a draggable window."

Fix: add UiElement.IsDragSource (virtual, default false); UiItemSlot overrides it to
`ItemId != 0` (occupancy-gated). UiRoot.OnMouseDown now prioritizes IsDragSource over
window-move — an OCCUPIED slot starts the item drag (promotes to BeginDrag on >3px),
an EMPTY slot falls through to the IA-12 whole-window-drag so the bar stays movable
by its empty cells / chrome. UiRoot stays item-agnostic (reads only the bool). This
REDUCES divergence (occupied cells now drag like retail) within IA-12's umbrella — no
new register row.

Regression tests reproduce the LIVE topology (slot inside a Draggable frame); the
earlier RootWithBoundSlot tests put the slot directly under the root, so they could
not catch it. Full suite 493 pass / 0 fail / 2 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:26:53 +02:00
Erik
1e53820dd7 docs(issues): #142 — empty item-slot press+drag+release emits a Click (latent)
Filed from the B.1 spine code review. No-op today (toolbar Clicked guards
ItemId!=0); deliberately NOT guarded with a speculative _dragCancelled bit
(would make item-slots differ from every other widget + guess at retail).
Verify retail's empty-cell press+move+release before changing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:12:48 +02:00
Erik
35ebb3c8f0 test(ui): D.5.3/B.1 — strengthen toolbar drag-handler test coverage
Cover OnDragOver ObjId==0 reject branch, make HandleDropRelease inertness assertion meaningful (populate first), and assert DragHandler/SourceKind on the bottom row too. Test-only; addresses code-review coverage gaps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:06:00 +02:00
Erik
3d04f61451 feat(ui): D.5.3/B.1 — toolbar drag handler stub + spine wiring (TS-33)
Implements Task 4 of the drag-drop spine: ToolbarController now satisfies
IItemListDragHandler. The ctor loop wires RegisterDragHandler(this) on every
slot list and stamps Cell.SlotIndex + Cell.SourceKind=ShortcutBar, matching
retail's gmToolbarUI::PostInit/RegisterItemListDragHandler pattern. OnDragOver
accepts any non-empty payload (TS-33 stub; eligibility gate is Stream B.2).
HandleDropRelease logs and returns (no AddShortcut 0x019C / RemoveShortcut
0x019D wire yet). Three new B.1 xUnit tests cover handler registration, accept,
and inert-stub semantics. All 490 app tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:00:20 +02:00
Erik
ef5049fd58 fix(lighting): #142 — per-instance sun gate for windowed-building interiors
Standing inside (or looking into) a windowed building like the Agent of
Arcanum, interior objects (furniture, NPCs, the player) were lit by the
directional sun because acdream's sun gate was per-FRAME (keyed on the
player being in a sealed cell), not per-DRAW as retail does it.

Retail's PView::DrawCells (0x005a4840) runs two stages per frame:
  outdoor stage   → useSunlightSet(1) (0x005a485a): sun ON
  interior stage  → useSunlightSet(0) (0x005a49f3): sun OFF
DrawMeshInternal (0x0059f398) then calls minimize_object_lighting only
when useSunlight==0, so indoor objects ALWAYS skip the sun regardless of
whether the player's cell is windowed or sealed.

Fix: add a per-instance uint SSBO (binding=6 instanceIndoor[]) whose value
is IndoorObjectReceivesTorches(ParentCellId) — the same predicate AP-43
already uses for the torch gate. In mesh_modern.vert, nest the sun loop
inside an additional `if (instanceIndoor[instanceIndex] == 0u)` check
inside the existing `if (uLightingMode == 0)` block. Indoor objects get
torches (unchanged) but now skip the sun; outdoor objects keep the sun and
still get no torches. The ambient regime (UpdateSunFromSky: 0.2 sealed /
sky otherwise) is untouched — it was already correct.

Mechanically: _currentEntityIndoor set once per entity in
ComputeEntityLightSet; appended to InstanceGroup.IndoorFlags in
AppendCurrentLightSet; grown/packed/uploaded in the same cursor loop as
_clipSlotData and _lightSetData; deleted in Dispose. Mode-1 draws
(EnvCellRenderer) never read binding=6 — the sun loop is inside the
uLightingMode==0 uniform-control-flow branch.

AP-43 divergence register updated: the sun half is now per-draw (no
longer a residual). Residual narrowed to the unaudited ebp_2 test in
CellManager::ChangePosition (no observed impact).

Tests: WbDrawDispatcherIndoorFlagTests pins IndoorObjectReceivesTorches
for the spec §5 representative ids: 0xA9B40172 (Agent of Arcanum EnvCell)
→ 1; 0xA9B40031 (land sub-cell) → 0; 0xA9B4FFFF (landblock) → 0; null
(outdoor shell) → 0; plus the boundary cases 0x0100/0x00FF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:55:56 +02:00
Erik
595c4bdac4 feat(ui): D.5.3/B.1 — UiRoot payload injection + cursor drag ghost (AP-47)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:51:01 +02:00
Erik
cd024377a0 docs(lighting): #142 spec — per-instance sun gate for windowed interiors
Decomp + in-game probe (agent-arcanum-probe.log) show the ambient regime
is already retail-faithful (CellManager::ChangePosition 0x004559B0); only
the sun is wrong — acdream gates it per-FRAME, retail per-STAGE
(useSunlight 0x0054d450). Fix = a per-instance indoor flag (reusing
IndoorObjectReceivesTorches, the AP-43 predicate) gating the sun off for
indoor mode-0 objects. No second ambient, UpdateSunFromSky + EnvCellRenderer
unchanged. User pre-approved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:49:35 +02:00
Erik
1672eaa620 feat(ui): D.5.3/B.1 — UiItemSlot drag source + drop target + accept/reject overlay
- UiElement: two new virtuals GetDragPayload()/GetDragGhost() (default null)
  keep UiRoot item-agnostic; any leaf can opt into drag by overriding these.
- UiItemSlot: SlotIndex + SourceKind properties for payload identity; two
  overrides return ItemDragPayload / icon ghost when the slot is occupied.
  FindList() walks the parent chain to locate the owning UiItemList and its
  registered IItemListDragHandler.
- UiItemSlot.OnEvent: MouseDown now just consumes the press; use-item fires
  on Click (mouse-up) so a drag doesn't also trigger the use-item callback.
  DragEnter → ask handler, set Accept/Reject overlay. DragOver → reset to
  None (fires on leave). DropReleased → clear overlay + dispatch to handler
  when Data0 == 1 (accepted). DragBegin consumed (source).
- OnDraw: accept/reject sprite overlay drawn last, guarded on id != 0 to
  avoid the resolve(0)-→-magenta footgun.
- ToolbarControllerTests: Click_emitsUseForBoundItem changed from MouseDown
  to Click to match the new dispatch.
- 12 new DragDropSpineTests pass; full suite 481/483 (2 pre-existing skips).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:40:53 +02:00
Erik
9d483468d9 feat(ui): D.5.3/B.1 — drag payload + handler interface + UiItemList registration
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:34:21 +02:00
Erik
24ce8a0b5e docs(D.5.3/B.1): drag-drop spine implementation plan
Bite-sized TDD plan for Stream B.1, four tasks matching the spec slices:
 1. ItemDragPayload + ItemDragSource + IItemListDragHandler + UiItemList
    handler registration.
 2. UiElement GetDragPayload/GetDragGhost virtuals + UiItemSlot drag source
    + drop target + accept/reject overlay; use moves MouseDown->Click (fixes
    the existing ToolbarControllerTests.Click test in-task to stay green).
 3. UiRoot payload pull + cancel-on-null + cursor ghost (AP-47).
 4. ToolbarController stub handler + spine wiring (TS-33) + registration tests.

Exact code, commands, and the AP-47/TS-33 register rows inline. Plan + spec
preapproved by the user; executing subagent-driven next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:31:37 +02:00
Erik
2de9cc1c19 docs(D.5.3/B.1): drag-drop spine design spec
Stream B.1 of the 2026-06-18 handoff — the shared widget-level drag-drop
infra that both shortcut-drag (B) and the inventory window (C) sit on.

Four design decisions confirmed with the user this session:
1. Payload = typed ItemDragPayload record snapshotted at drag-begin
   (ObjId/SourceKind/SourceSlot/SourceCell); SourceContainer derived at
   drop from ClientObjectTable (single source of truth).
2. Cursor ghost painted by UiRoot via a generic UiElement.GetDragGhost()
   hook — keeps UiRoot item-agnostic; floats above all windows.
3. Cell (UiItemSlot) is the drop-target hit unit + accept/reject overlay
   owner, delegating the decision + dispatch UP to its parent UiItemList's
   registered IItemListDragHandler (faithful to retail cell->ItemList_DragOver
   ->m_dragHandler; scales to the inventory N-cell grid).
4. PR ships infra + a visible toolbar STUB handler (logs, no wire) so the
   ghost/overlay/dispatch are confirmable this session; AddShortcut/Remove
   wire is Stream B.2.

Retail-grounded: InqDropIconInfo flags (&0xE==0 fresh / &4 reorder, reject
state 0x10000040) confirmed live at gmToolbarUI 0x004bd162; the cell
begin-drag/CatchDroppedItem/RegisterItemListDragHandler chain at decomp
229344/229744/230461. Planned register rows: AP-47 (ghost reuses full icon
at reduced alpha vs retail m_pDragIcon) + TS-33 (toolbar drop stub pending
B.2). No new wire format in the spine itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:23:44 +02:00
Erik
f7f3e0887b docs(lighting): indoor lighting regime handoff — file #142 (windowed-interior regime) + #143 (portal dynamic light)
Clean handoff for the next M1.5 "indoor world feels right" session, picking up
the two indoor-lighting gaps the user spotted at the #140 visual gate.

#142 (PRIMARY): windowed-building interiors + look-ins read "like outdoors".
Root cause grounded: retail's lighting regime is per-DRAW-STAGE (PView::DrawCells
draws ALL EnvCells in the useSunlightSet(0) interior stage — torch-lit, no sun,
regardless of SeenOutside), while acdream's is a per-FRAME global keyed on the
player's cell (playerInsideCell). So acdream's windowed interiors (SeenOutside)
+ look-ins stay in the outdoor regime. This is the AP-43 residual surfaced.
Fix direction: make sun+ambient per-draw like AP-43's torches (design fork laid
out for a brainstorm). Resolves AP-43.

#143 (SECONDARY): portal swirl casts no light. acdream registers only static
Setup.Lights; the portal is a retail DYNAMIC light (add_dynamic_light ->
minimize_envcell_lighting). Fix: register a dynamic LightSource for portals.

Handoff doc carries the verified retail decomp (useSunlightSet/PView::DrawCells
stages), current acdream line refs, the three gaps, the fix fork, validation
plan, and DO-NOT-RETRY. Neither issue is a regression from #140.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:17:59 +02:00
Erik
31d7ffd253 merge: bring main (A7 lighting Fix A–D + UN-7 + #140 Fix D) into the D.5 branch
Integrates main's 19 commits (A7 outdoor/indoor torch lighting Fix A/B/C/D,
GlobalLightPacker, shader updates, UN-7) under the D.5 toolbar/item-model stack
(D.5.1/D.5.2/D.5.4/D.5.3a). Auto-merged cleanly except docs/ISSUES.md.

Conflict resolved: both lineages used #140 for different issues. Kept main's
#140 = "A7 Fix D" (resolved); renumbered the toolbar/selected-object issue to
#141 (note added; this branch's commits/spec still reference #140 — immutable).
The register auto-merged (AP-46 cites file:line, not #140; UN-7 keeps #140=Fix D).

Build + full suite green on the merged tree (2,713 passed / 4 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 12:01:20 +02:00
Erik
711c2ea688 docs(D.5.3a): #140 — health+name+flash done & visually confirmed
Selected-object meter health half passed the visual gate (2026-06-20): name on
the black band, attackable-only health gate, UpdateHealth-driven bar, green flash,
no magenta. Mana (0x100001A2) + stack entry/slider (0x100001A3/A4) remain deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 09:39:42 +02:00
Erik
07965852e0 chore(cli): UI-debug apparatus — mock-selbar, dump-edges, crop, probe
Standalone AcDream.Cli subcommands built during the D.5.3a visual gate, kept as
reusable UI/sprite/framebuffer debugging apparatus (alongside the existing
export-ui-sprite / dump-sprite-sheet / render-vitals-mockup tools):
- mock-selbar: composite the selected-object health bar (back + fill at fractions)
- dump-edges: print a sprite's first/last column RGB at every row
- crop: crop + nearest-upscale a region of a PNG (zoom into a framebuffer dump)
- probe: print the RGB of a pixel block from a PNG

Dev-only (reached via explicit args[0]); no game-runtime impact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 09:37:29 +02:00