Commit graph

740 commits

Author SHA1 Message Date
Erik
79ee3ffbe2 feat(studio): RenderBootstrap — shared render stack for the UI previewer
Extracts the subset of GameWindow.OnLoad the UI Studio needs into a
standalone RenderBootstrap.Create factory: bindless detection, shaderDir,
SceneLightingUboBinding, mesh shader, TextureCache, animLoader,
WbMeshAdapter, SequencerFactory, EntitySpawnAdapter,
EntityClassificationCache, WbDrawDispatcher (+ A2C gate from
QualitySettings), UiDatFont load, and UiHost.  No terrain / sky /
physics / streaming — only the pieces listed in the RenderStack record.

GameWindow is untouched; this is additive new code only.

Note: task spec listed VitalsDatFont as AcDream.App.UI.Layout.UiDatFont
but the type lives in AcDream.App.UI — corrected here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 14:17:43 +02:00
Erik
8fa66c23d5 fix(D.2b): Slice 2 — retail-exact doll pose, camera + heading (visual gate)
The paperdoll doll now matches retail: correct held pose, framing, and
facing. Three decomp-sourced fixes closed the visual gate.

Pose: cdb-confirmed m_didAnimation = 0x030003C0 (gmPaperDollUI), played
once + HELD (set_sequence_animation framerate=0, RedressCreature
0x004a3c22). Dumping the dat showed the 29-frame anim has only two
distinct keyframes — frame 0 (transitional, bent arm) and frames 1..28
(byte-identical: the settled stance, arms down + leg back) — so
ApplyPaperdollPose applies the LAST frame statically (no looping).

Camera: ported verbatim from UIElement_Viewport::SetCamera (decomp
0x004a5a39). position (0.12,-2.4,0.88); direction (0,0,0) => IDENTITY
view frame => look straight down +Y, ZERO yaw; FOV pi/4 (CreatureMode
ctor default 0x004543cf); ambient 0.3. The prior hand-tune aimed the
camera at mid-body, adding a ~2deg yaw that turned the doll's face away
— full-body framing comes from eye-height + FOV, not aiming.

Heading: retail Frame::set_heading(h) (0x00535e40) builds facing
(sin h, cos h); System.Numerics CreateFromAxisAngle(+Z, +h) rotates the
body's default +Y forward to (-sin h, cos h) — the X-lean was MIRRORED
(~22deg), the real cause of the turned-away face. Negate the angle to
land on retail's facing.

Wrap-up: stripped the temporary O/P pose-frame stepper + Slice2
diagnostics; divergence register AP-66 reworded, AP-67 (RTT doll render
vs in-cell CreatureMode::Render) + AP-68 (per-race UpdateForRace
unimpl) added; DollCameraTests pinned to the retail values + a zero-yaw
guard. tools/cdb/paperdoll-pose.cdb = the pose-DID capture script.
Build + full suite green (Core 1579 / Core.Net 343 / App 597 / UI 425).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 13:16:27 +02:00
Erik
fe319bd2aa fix(D.2b): Slice 2 visual gate — frame the whole doll + caption the Slots toggle
Visual gate 1 (user): the doll rendered but only the legs showed (camera
aimed at the model origin = the feet) and the Slots button was invisible.

- DollCamera: aim the look-at at mid-body (~0.95 m) and stand back ~3.7 m
  so the whole ~1.9 m figure fits. (Size is a later retail-comparison
  polish per the user.)
- PaperdollController: the Slots button (0x100005BE) is found + wired
  (diagnostic confirmed armorSlots=9/9, viewport=UiViewport,
  slotsButton=UiButton) but its dat element has no face sprite, so it drew
  nothing. Give it a gold "Slots" caption (UiButton.Label, like chat Send)
  via a new datFont param on Bind. Temporary [Slice2-paperdoll] diagnostic
  logs the button rect + the found widgets (stripped at wrap-up).

Idle animation deferred to a focused follow-up (faithful idle needs a full
AnimatedEntity + Sequencer through the TickAnimations multi-branch path +
re-dress coordination — real integration risk vs the verified static doll).

Build + full App suite green (594).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:58:22 +02:00
Erik
8b0365e7a9 feat(D.2b): Slice 2 — wire paperdoll doll RTT pass + re-dress into GameWindow
Drives the doll 3-D pass in a pre-UI hook (after the world passes, before
_uiHost.Draw), gated on inventory-open AND doll-view (the viewport widget's
Visible, set by the Slots toggle). RefreshPaperdollDoll clones the live
player entity (_entitiesByServerGuid[player]) into a DollEntityBuilder doll
with COPIED MeshRefs (frozen pose); re-dress is triggered by a dirty flag
set in OnLiveAppearanceUpdated (0xF625) — the C# analog of RedressCreature.

Correctness fix: the doll is passed in animatedEntityIds so the dispatcher
BYPASSES the Tier-1 classification cache (WbDrawDispatcher.cs:1142). Without
it, a re-dress (a new WorldEntity with the same fixed DollRenderId) would
serve the previous doll's cached batches and the new gear wouldn't appear.

Renderer disposed in the GameWindow teardown. Build + full App suite green
(594). Static doll (no idle animation yet — next slice). Visual gate next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:22:45 +02:00
Erik
3cdecb536b feat(D.2b): Slice 2 — PaperdollViewportRenderer RTT pass (static doll)
The C# analog of CreatureMode::Render: draws one re-dressed player clone
into a private FBO (RGBA8 + depth24-stencil8) with the fixed DollCamera +
one distant light (retail 0.3,1.9,0.65 @ 2.0), sealed in a GLStateScope so
it can't disturb world/UI GL state. frustum:null ⇒ the doll's synthetic
landblock is always visible ⇒ walked from entry.Entities and drawn from its
current MeshRefs (static pose; idle animation is a later slice).

Manages the FBO directly via GL (ManagedGLFramebuffer is unused/bitrotted
WB infra). UiViewport blit flips V (FBO bottom-left origin vs UI top-left)
so the doll isn't upside-down. Not yet wired — GameWindow hook is next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:14:20 +02:00
Erik
362d41aacf feat(D.2b): Slice 2 — DollEntityBuilder (player Setup+ObjDesc -> doll WorldEntity)
Mirrors the palette/part-override mapping at GameWindow.cs:3390-3431 in a
testable static helper. Build() accepts plain (SubPaletteId, Offset, Length)
and (PartIndex, GfxObjId) tuples, builds PaletteOverride only when
subPalettes.Count > 0 (same gate as GameWindow), and poses the entity at
origin facing the viewer (191.367905° / +Z). Reserved synthetic guid
0xDA11D011 keeps the doll distinct from the live player and satisfies
EntitySpawnAdapter's ServerGuid != 0 guard. 7 new tests green; suite 594/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:07:02 +02:00
Erik
10cb31223f feat(D.2b): Slice 2 — DollCamera (fixed paperdoll ICamera)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 09:00:27 +02:00
Erik
a4c68520ea fix(D.2b): paperdoll empty slots show a visible frame (not transparent)
User correction at the visual gate: the green "figure" in the paperdoll is
the LIVE 3D character (the doll — can be naked), NOT per-slot silhouettes.
The default view (Slots button OFF) = the doll + non-armor slots; pressing
Slots hides the doll and shows the armor slots. So the doll + the Slots
toggle are Slice 2 (the UiViewport); there are no per-slot silhouette sprites
to chase.

For Slice 1 (no doll yet) the right empty-slot look is simply a VISIBLE FRAME
so every slot position can be seen + used — which fixes the "I see only slots
with equipment, no empty slots" report. The earlier transparent (EmptySprite=0)
came from the stale silhouette assumption. PaperdollController now takes an
emptySlotSprite; GameWindow passes the inventory grid's empty square
(0x06004D20) for a consistent visible frame.

App suite 580 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 00:02:11 +02:00
Erik
058da60212 feat(D.2b): wire PaperdollController into the inventory frame (Slice 1)
Adds the _paperdollController field and PaperdollController.Bind(...)
call in the inventory-frame block alongside InventoryController, wiring
up the paperdoll equip slots with their icon composer and wield action.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:35:51 +02:00
Erik
124a4a2efa feat(D.2b): wire InventoryController drag-drop to the live session
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:55:20 +02:00
Erik
ba5f974f56 feat(D.2b): wire InventoryController container-open callbacks to the live session
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:32:56 +02:00
Erik
edbe5bafd5 feat(ui): D.2b empty-slot art — GameWindow resolves + wires per-list empty sprites
Inventory contents grid / side-bag / main-pack cells now render the retail
pack-slot empty art (dat cell template via ItemListCellTemplate) instead of the
generic toolbar square. Divergence rows AP-55 (toolbar still hardcoded) + AP-56
(flat single-sprite cell vs retail''s layered prototype).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:07:58 +02:00
Erik
a1e7041ba8 feat(ui): D.2b inventory finish — vertical-only window resize
Drag the bottom edge to expand the inventory and see more of the pack
(ResizeX=false + ResizableEdges=Bottom blocks horizontal resize). The contents
grid + its gm3DItemsUI sub-window + scrollbar + the backdrop stretch vertically
(Left|Top|Bottom); the paperdoll + side-bag column stay pinned at the top
(Left|Top). The grid's ViewHeight grows → more rows + the scrollbar thumb ratio
(view/content) reflects how much is left to scroll. Min = dat default (expand
only for now); max = 560. Visually confirmed: scroll + resize work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 21:17:28 +02:00
Erik
06c4288282 feat(ui): D.2b inventory finish — window chrome frame (UiNineSlicePanel)
Wrap the gmInventoryUI content in the universal 8-piece beveled chrome (same
UiNineSlicePanel as vitals/chat/toolbar), so the inventory has a proper window
frame like every other window. The frame is the draggable window + the F12-
toggled registered window; the dat content sits inside it offset by the border.
Reused as the base for the vertical-resize step next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:55:50 +02:00
Erik
102c46c8e3 feat(app): D.2b-B B-Wire — wire player guid into ObjectTableWiring + GameEventWiring
Pass () => _playerServerGuid to ObjectTableWiring.Wire and GameEventWiring.WireAll
so the new Batch 5/6 playerGuid parameters are populated. These were previously
left as null (default), meaning PD property upserts and PrivateUpdatePropertyInt
delivery would not route to the correct player object. Task 14 of the B-Wire plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:08:07 +02:00
Erik
03fbf4464d feat(ui): D.2b-B — wire InventoryController into the inventory-init block
Adds _inventoryController field (after _selectedObjectController) and calls
InventoryController.Bind in the existing inventory-init block, between
RegisterWindow and the Console.WriteLine. Uses Objects/iconComposer/
vitalsDatFont already in scope; strength lambda pattern-matches AttributeSnapshot
to int? (uint? has no implicit cast to int?).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 09:12:34 +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
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
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
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
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
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
8f627cce0e fix(D.5.3a): selected-object meter visual-gate fixes (name, gate, flash, magenta)
Visual gate against retail surfaced several fidelity gaps in the selected-object
strip; all fixed and user-confirmed. Faithful to gmToolbarUI::HandleSelectionChanged
(acclient_2013_pseudo_c.txt:198635) + RecvNotice_UpdateObjectHealth (:196213).

- UiMeter.DrawHBar: guard each slice on `id != 0` BEFORE resolve. resolve(0)
  returns the 1x1 magenta placeholder with a non-zero GL handle, so the single-
  image meter (caps id=0) was drawing 1px magenta caps at the bar's ends. The
  3-slice vitals meter (all ids set) was unaffected. (the magenta-lines bug)
- SelectedObjectController: meter visibility is now UpdateHealth-driven (shown when
  health is known for the selected guid — HasHealth at select or HealthChanged),
  not shown-on-select; brief green selection flash via Tick revert; overlay floated
  above the meter so the flash isn't hidden by the bar; name top-aligned into the
  bar sprite's black band (NameBandHeight) with the bar below.
- GameWindow.IsHealthBarTarget: gate the health bar on the server PWD bits
  BF_ATTACKABLE (0x10) | BF_PLAYER (0x8) — friendly/vendor NPCs and attackable
  Doors (Misc type) are name-only; players/monsters get the bar. Replaces the
  too-loose IsLiveCreatureTarget. Wired SelectedObjectController.Tick in OnUpdate.
- CombatState.HasHealth(guid): distinguishes a known health value from the 1.0
  default, so a re-selected already-assessed target shows its bar immediately.
- TextureCache.GetOrUploadRenderSurface: resolve the surface's DefaultPaletteId
  so paletted (P8/INDEX16) UI sprites decode instead of falling to magenta.
- ToolbarController.HiddenIds: also hide 0x100001A3 (stack-entry box) — retail
  hides it in HandleSelectionChanged; it was rendering as a stray black box.

Divergence register: AP-47 (meter-visible timing) retired (now faithful); AP-46
rewritten to the BF_ATTACKABLE/BF_PLAYER gate approximation. Full suite green
(2,688 passed / 4 skipped). User-confirmed: name on top, NPC name-only, monster
bar on assess, green flash, no magenta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 09:37:15 +02:00
Erik
c83fd02642 merge: bring main (UN-7, #140 filing, D.2b UI rows) into A7 Fix D round-2 branch
Resolves the divergence-register conflict: kept the accurate per-VERTEX AP-35
(Fix A shipped per-vertex; main's row was the stale pre-Fix-A per-pixel text),
kept main's UI rows AP-37..AP-42, and renumbered this branch's torch-gate row
AP-37 -> AP-43 (AP-37 was taken by main's LayoutDesc row). AP count 41 -> 42.
Retargeted the AP-37 references in WbDrawDispatcher + the CHECKPOINT to AP-43.
Marked ISSUES #140 RESOLVED (b7d655b) with the corrected root cause.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 09:29:53 +02:00
Erik
b7d655bce7 fix(lighting): A7 Fix D round 2 — outdoor objects get NO torches (retail useSunlight gate) (#140)
The Holtburg meeting-hall facade washed out warm/bright vs retail. The round-1
checkpoint blamed torch REACH (acdream Falloff 6×1.3=7.8m vs a supposed retail
Falloff 4). That theory is WRONG, and this commit fixes the real cause.

Empirical (HoltburgTorchFalloffProbeTests, headless dat dump via the production
LightInfoLoader): the orange entrance torch (setup 0x020005D8) is raw dat
Falloff 6 and acdream reads it FAITHFULLY — there is no Falloff-4 torch anywhere
in Holtburg. Both clients read the same dat float, so reach was never inflated.

Decomp (read verbatim + corroborated by an independent adversarial workflow):
retail's per-object torch binder minimize_object_lighting (0x0054d480) is gated
in RenderDeviceD3D::DrawMeshInternal (0x0059f398) by `if (Render::useSunlight == 0)`.
The outdoor landscape stage runs useSunlightSet(1) (PView::DrawCells 0x005a485a,
before LScape::draw), so the building EXTERIOR shell — drawn via
DrawBlock→DrawSortCell→DrawBuilding→CPhysicsPart::Draw→DrawMeshInternal — is lit
by SUN + ambient ONLY; torches are SKIPPED. The static bake
(SetStaticLightingVertexColors 0x0059cfe0) is EnvCell-only. So retail NEVER
torch-lights outdoor objects. This exactly explains the isolation test (object
point lights OFF → building matches retail).

Fix: WbDrawDispatcher.ComputeEntityLightSet gates per-object torch selection on
the object being INDOOR (ParentCellId is an EnvCell, (id&0xFFFF)>=0x0100) via the
pure predicate IndoorObjectReceivesTorches. Outdoor objects (building shells with
null ParentCellId, outdoor scenery, outdoor creatures) keep the all-(-1) light
set ⇒ sun + ambient only = retail. The indoor "no sun" half is already handled by
the global sun-kill when the player is inside a cell (UpdateSunFromSky). No
dungeon regression: EnvCell statics get ParentCellId set (keep torches).

Divergence register: AP-37 (residual: acdream keys sun/torch on the object's own
cell + a per-frame player-inside sun-kill, vs retail's per-draw-stage useSunlight;
only matters for through-doorway look-ins). The round-1 CHECKPOINT got a RESOLVED
banner correcting the reach theory.

Tests: WbDrawDispatcherTorchGateTests (7), HoltburgTorchFalloffProbeTests (dat
dump). App 280/1skip, Core 1486/2skip green. Held at the visual gate — not merged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 23:56:49 +02:00
Erik
6636e50c2a feat(D.5.3a): selected-object meter — Health bar + name on the action bar
Port of gmToolbarUI::HandleSelectionChanged (acclient_2013_pseudo_c.txt:198635).
When the player selects a world object the action bar's bottom strip shows the
object name + (for player/pet/attackable targets) a live Health meter; deselect
clears it. Mana (#140) + stack slider deferred.

- SelectedObjectController (new): clear-then-populate on selection change; sets
  name (UiText child, VitalsController pattern), overlay state (ObjectSelected /
  StackedItemSelected via UiDatElement.ActiveState), shows the health meter and
  sends QueryHealth for health targets. Subscribes via a delegate seam (no
  GameWindow coupling).
- GameWindow: _selectedGuid field -> SelectedGuid property + SelectionChanged
  event (fires on actual change only); 3 write sites converted, reads untouched.
  All selection-write paths (LMB pick, Tab/Q, despawn-clear via Tick()) run on
  the render thread, so the event-driven UI mutation is single-threaded.
- WorldSession.SendQueryHealth (0x01BF) — wraps SocialActions.BuildQueryHealth.
- DatWidgetFactory.BuildMeter: handle the single-image toolbar meter shape
  (back-track on the element's own DirectState, fill on one Type-3 child). The
  sprites go in the TILE slot (DrawMode=Normal tiles to full bar geometry per
  UIElement_Meter::DrawChildren) — a left-cap assignment would gap/clamp a
  sub-140px sprite. Vitals 3-slice path unchanged.
- ToolbarController.HiddenIds: A1 (health) now owned by SelectedObjectController;
  A2 (mana) + A4 (stack) stay hidden (deferred) so their dat back-tracks don't
  render as stray empty bars.

Adversarial Opus review found + fixed: the mana-meter orphan (A2 left unhidden)
and the meter tile-vs-cap render bug (C1). Divergence rows AP-46 (health gate
approximation: IsLiveCreatureTarget vs IsPlayer||pet||attackable) + AP-47
(meter shown on select vs on UpdateHealth reply). Spec §5 corrected.

Build + full test suite green (2,684 passed / 4 skipped). Health meter render
fidelity (full-width fill + fraction mapping) pending the user's visual gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 22:47:24 +02:00
Erik
0980bea48d fix(render): A7 Fix D D-3/D-4 — two-path lighting (objects plain-Lambert+sun, EnvCell wrap+no-sun) (#140)
mesh_modern unified all meshes into one calc_point_light path: it applied the
bake's half-Lambert wrap to objects (lighting character backs from a torch behind
them) and added the sun to EnvCell building shells (warm facade wash). Retail
splits these: objects = hardware plain Lambert max(0,N.L) + sun; EnvCell walls =
baked wrap, dynamics only, NO sun (minimize_envcell_lighting). Add a per-draw
uLightingMode (WbDrawDispatcher=0 object, EnvCellRenderer=1 envcell) selecting the
angular term (wrap vs plain Lambert) and gating the sun. Per-light cap + D-1 clamp
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:38:30 +02:00
Erik
c62da825fe fix(render): A7 Fix D D-2 — EnvCell shell binds its own per-cell light set (#140)
The cell shell read whatever light set (SSBO 4/5) WbDrawDispatcher last left
bound, lighting walls with a leaked set. EnvCellRenderer now uploads its own
binding=4 global lights (frame PointSnapshot via GlobalLightPacker) + a binding=5
per-instance set, computed per cell by LightManager.SelectForObject over the
cell's world bounds (mirrors _cellIdToSlot + WbDrawDispatcher.ComputeEntityLightSet).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:35:33 +02:00
Erik
cf62793304 fix(render): A7 Fix D D-1 — clamp the point-light sum on its own (#140)
accumulateLights folded ambient+sun+torches into one accumulator clamped only
in the frag, so a few warm intensity-100 torches blew walls/objects to white.
Mirror retail SetStaticLightingVertexColors: sum point/spot into pointAcc, clamp
to [0,1] (the baked emissive), THEN add ambient+sun, frag final-clamps. Matches
LightBake.ComputeVertexColor (LightBakeConformanceTests). Per-light cap unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:29:45 +02:00
Erik
180b4af2a9 refactor(lighting): extract GlobalLightPacker (shared binding=4 layout) — A7 Fix D prep
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:25:11 +02:00
Erik
a9d40addac refactor(D.5.4): retire _liveEntityInfoByGuid; selection resolves from ClientObjectTable
The one weenie table now holds every object's name+type, so the redundant
Name+ItemType dictionary is gone (retail: one weenie_object_table).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:48:13 +02:00
Erik
82f5968316 feat(D.5.4): ObjectTableWiring (CreateObject=upsert, Delete=evict, 0x02CE) off GameWindow
CreateObject ingestion moves to Core.Net; GameWindow drops the EnrichItem call +
inline 0x02CE handler. Fixes the Coldeve blank-icon root cause: items with no PD
stub are now created, not dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:34:56 +02:00
Erik
b506f53633 refactor(D.5.4): rename ItemRepository->ClientObjectTable, ItemInstance->ClientObject
Broaden naming to the data side of every server object (retail weenie_object_table
shape). Pure rename; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:36:16 +02:00
Erik
2f789da73d feat(D.5.2): route live UiEffects updates (0x02CE) to the item icon
Subscribe ObjectIntPropertyUpdated (added in Group C Task 4) in GameWindow
next to the existing VitalUpdated/VitalCurrentUpdated subscriptions. Routes
PublicUpdatePropertyInt(0x02CE) UiEffects (property 18) → ItemRepository.
UpdateIntProperty → ItemInstance.Effects → ItemPropertiesUpdated → UiItemSlot
re-composites the icon in real time. The end-to-end path is the visual-
verification acceptance test (live ACE server + a draining magical item).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:47:54 +02:00
Erik
e0dce5aa9f feat(D.5.2): IconComposer 2-stage effect composite + 5-arg GetIcon
Widen the cache key to (typeUnderlay, icon, underlay, overlay, effects).
GetIcon is now a 2-stage composite mirroring retail IconData::RenderIcons
(0x0058d180): Stage 1 builds the drag composite (base + overlay) and,
when effects != 0, ReplaceColorWhite tints it with the effect tile's
mean-opaque color (DR-1: tint SOURCE, not blit; DR-3: zero-effects
black path skipped). Stage 2 blits typeUnderlay + custom underlay +
drag into the final cached GL texture.

Both callers updated: ToolbarController Func arity widened to 6-arg
(passes item.Effects); GameWindow closure and OnLiveEntitySpawned
EnrichItem call pass spawn.UiEffects. Tree builds with 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:40:37 +02:00
Erik
0e7a083da6 chore(D.5.1): remove temp geometry probe + add RestrictionDB-skip parse test
Task 1: remove the [D.5.1 PROBE] bottom-right rect-dump block from the
toolbar mount in GameWindow.cs. The block iterated 7 element ids and
logged ScreenPosition/Width/Height/Type; it was marked temporary and is
now superseded by the chrome window-frame fix. The kept [D.5.1] startup
diagnostic Console.WriteLines (digit arrays, toolbar ready, window from
LayoutDesc) are untouched.

Task 2: add TryParse_HouseRestrictionsSkipped_ThenIconOverlayCaptured to
CreateObjectTests.cs. Exercises the variable-length RestrictionDB skip
(weenieFlags bit 0x04000000: 12-byte fixed header + 4-byte hash-table
header + count*8 entries) followed immediately by IconOverlay (0x40000000)
and IconUnderlay (weenieFlags2 0x01 via IncludesSecondHeader 0x04000000).
Proves the skip lands the cursor at the right position for both capture
fields. 301/301 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:13:46 +02:00
Erik
b37db79a23 feat(D.5.1): wrap toolbar in UiNineSlicePanel chrome frame (mirrors chat window)
The toolbar LayoutDesc (0x21000016, 300x122) was mounted bare — no window
chrome. This commit wraps it in a UiNineSlicePanel (the same 8-piece bevel +
gold grip chrome used by the vitals and chat windows), matching the pattern at
GameWindow.cs ~line 1885 verbatim.

- toolbarFrame is the top-level UiNineSlicePanel (Draggable=true, Anchors=None
  per UiNineSlicePanel ctor defaults). Outer size = 310x132 (300+2*5 x 122+2*5).
- toolbarRoot sits inside at offset (5,5) — the border thickness — with all-edge
  anchors so it reflows if the frame is resized. Draggable=false, Resizable=false
  on the content (only the frame is the drag handle).
- The frame's right border (x=305..310 screen) covers the row-2 right cap
  overhang (~2px past the content edge at x=300..302), since the border region
  starts at content_right=300 and extends to frame_right=310.
- Probe block untouched: still calls toolbarLayout.FindElement for diagnostic ids.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:53:53 +02:00
Erik
8d49042909 fix(D.5.1): read empty-slot background digits from composite 0x10000341 (0x1000005e)
The empty/background digit array (property 0x1000005e) lives under cell composite 0x10000341, not 0x10000346 where peace/war are read; reading it from the wrong composite returned 0 entries so empty top-row slots showed no number. Live dat probe confirmed: 0x10000341 element 0x1000034A property 0x1000005e = 0x060010FA..0x06001102 (digits 1-9) + 0x060074CF (bottom row). Now empty top-row slots show the faint background number, occupied show the dark-box peace/war digit (decomp UIElement_UIItem::SetShortcutNum:229481/229493).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:34:18 +02:00
Erik
8a42066192 feat(D.5.1): parse item IconOverlay/IconUnderlay from CreateObject -> faithful icon overlay layer
The CreateObject optional-tail walker previously stopped at UseRadius (~20 fields
before IconOverlay). This left ItemInstance.IconOverlayId/IconUnderlayId always 0,
so IconComposer's underlay/overlay layers were never drawn on toolbar icons.

Exact field order verified against ACE WorldObject_Networking.cs:87-219 (the
serializer is the authority; acdream connects to a local ACE server):
  UseRadius → TargetType(u32) → UiEffects(u32) → CombatUse(sbyte) →
  Structure(u16) → MaxStructure(u16) → StackSize(u16) → MaxStackSize(u16) →
  Container(u32) → Wielder(u32) → ValidLocations(u32) →
  CurrentlyWieldedLocation(u32) → Priority(u32) → RadarBlipColor(u8) →
  RadarBehavior(u8) → PScript(u16) → Workmanship(f32) → Burden(u16) →
  Spell(u16) → HouseOwner(u32) → HouseRestrictions(variable RestrictionDB) →
  HookItemTypes(u32) → Monarch(u32) → HookType(u16) →
  IconOverlay(PackedDwordKnownType) ← CAPTURE →
  IconUnderlay from weenieFlags2 bit 0x01 ← CAPTURE

RestrictionDB handled correctly: Version(u32) + OpenStatus(u32) + MonarchId(u32)
+ count(u16) + numBuckets(u16) + count×8 bytes entries. Length-aware skip, not a
fixed constant.

weenieFlags2 is now CAPTURED (not skipped) when IncludesSecondHeader
(objDescFlags bit 0x04000000) is set, so the IconUnderlay bit can be tested.

The entire extended walk is inside try/catch: truncated packets degrade to
IconOverlayId=0 / IconUnderlayId=0 (no overlay drawn), never corrupting.

Threading: CreateObject.Parsed → WorldSession.EntitySpawn → GameWindow
OnLiveEntitySpawned → Items.EnrichItem — both ids thread through all three
seams. EnrichItem extended with optional iconOverlayId + iconUnderlayId params
(defaulted 0, backward-compatible).

No change to IconComposer or ToolbarController (they already consume the ids).

Tests: 4 new CreateObject tests (IconOverlay only, overlay+underlay, no-overlay
regression, intermediate-fields cursor arithmetic). Full suite: 0 failures,
2636 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:34:47 +02:00
Erik
a7cad5566b fix(D.5.1): occupancy-gated slot numbers (empty=0x1000005e bg digit) + bottom-right rect probe
FIX 1: UIElement_UIItem::SetShortcutNum (decomp 229481) has a three-way source
branch: occupied+peace -> 0x10000042 (peace digit set), occupied+war -> 0x10000043
(war digit set), empty (ItemId==0) -> 0x1000005e (background digit, stance-independent).
acdream previously only had the peace/war pair and drew them regardless of occupancy.

Changes:
- GameWindow.cs: read property 0x1000005e into toolbarEmptyDigits (no fallback;
  null is safe). Logs entry count. Passes emptyDigits to Bind. Adds [D.5.1 probe]
  block logging screen pos + size of 7 bottom-right element ids via ScreenPosition.
- ToolbarController.cs: adds _emptyDigits field, emptyDigits ctor+Bind param (null
  default). RestampShortcutNumbers sets cell.EmptyDigits. Comments cite decomp 229481.
- UiItemSlot.cs: adds EmptyDigits property + ActiveDigitArray() internal testable seam
  (occupied -> peace/war by stance; empty -> EmptyDigits). OnDraw uses it. Comment
  updated with three-way source table.
- Tests: 5 new UiItemSlotTests (ActiveDigitArray occupancy), 2 new
  ToolbarControllerTests (emptyDigits injection + null-safe).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:27:27 +02:00
Erik
7d5a88cd15 fix(D.5.1): toolbar digit-array log reports post-fallback final state (review) 2026-06-17 13:58:07 +02:00
Erik
b2a812d1fa feat(D.5.1): faithful toolbar slot numbers 1-9 (SetShortcutNum digit sprites, peace/war)
Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465) and
gmToolbarUI::RecvNotice_SetCombatMode (196610-196621). The 9 top-row toolbar slots
show digit labels 1-9 at all times (even when empty — confirmed from the user''s
retail screenshot). The digit sprites are real 32x32 PFID_A8R8G8B8 RenderSurfaces
with glyphs baked into the top-left corner (rest alpha=0), drawn Alphablend over
the slot icon/empty sprite.

Digit DID arrays (peace: property 0x10000042, war: 0x10000043) are read at startup
from LayoutDesc 0x21000037 element 0x1000034A under composite 0x10000346 using the
same ArrayBaseProperty{DataIdBaseProperty} pattern as LayoutImporter.ReadState.
A cited-constant fallback (same confirmed dat ids) is used if the dat navigation
fails. The war glyph set (darker/golden glyphs) switches on any combat stance;
peace glyphs (lighter) restore on NonCombat — re-stamped by RestampShortcutNumbers()
called from both Populate() and SetCombatMode().

Changes:
- UiItemSlot: ShortcutNum/ShortcutPeace/PeaceDigits/WarDigits state; SetShortcutNum/
  ClearShortcutNum; OnDraw restructured (no early return) so digit draws after icon.
- ToolbarController: _peaceDigits/_warDigits/_peace fields; Bind() gains peaceDigits/
  warDigits optional params; RestampShortcutNumbers() helper; Populate() and
  SetCombatMode() both call RestampShortcutNumbers().
- GameWindow: reads digit arrays under _datLock from LayoutDesc 0x21000037, passes to
  Bind(); cited constants as fallback.
- Tests: 5 new UiItemSlotTests (SetShortcutNum/ClearShortcutNum state); 4 new
  ToolbarControllerTests (top-row/bottom-row labels, peace/war switch, array injection).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:52:50 +02:00
Erik
f21dbfad80 feat(D.5.1): faithful item-icon type-default underlay (EnumIDMap 0x10000004) — opaque icon backing
Retail IconData::RenderIcons (decomp 407524) builds the icon layer stack bottom→top:
type-default underlay (OPAQUE, Blit_Normal) first, then custom underlay, base icon,
custom overlay.  acdream's IconComposer omitted the type-default underlay, leaving
filled toolbar slots with a transparent background.

Resolution via the two-level EnumIDMap chain that retail uses (DBCache::GetDIDFromEnum
0x413940): Portal.Header.MasterMapId (0x25000000) → master[0x10000004] → submap DID
(0x25000008) → submap[LSB(itemType)+1] → 0x06 RenderSurface underlay DID.  Golden
values confirmed against the live dats: MeleeWeapon→0x060011CB, Armor→0x060011CF,
Clothing→0x060011F3, Jewelry→0x060011D5, None(fallback 0x21)→0x060011D4.

Changes:
- IconComposer: add ResolveUnderlayDid(ItemType)/EnsureUnderlaySubMap (memoised);
  widen cache key from (uint,uint,uint)→(uint,uint,uint,uint); GetIcon gains ItemType
  param and prepends the opaque underlay as layer 0 (Compose sizes to it → fully opaque)
- ToolbarController: widen _iconIds Func from 3-arg to 4-arg; Populate passes item.Type
- GameWindow: update toolbar mount lambda to 4-arg form
- Tests: update ToolbarController test stubs to (_,_,_,_); add
  Compose_opaqueUnderlayFirst_resultIsFullyOpaque (dat-free) and
  ResolveUnderlayDid_goldenValues_matchDat (dat-gated, skip when dats absent)

No divergence-register row existed for this omission; none added (fully ported now).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:37:53 +02:00
Erik
bfc452d610 fix(D.5.1): toolbar movable + chrome-grab + peace-only indicator + no prototype square
D1 — Toolbar not movable: toolbarRoot.Anchors = AnchorEdges.None (was Left|Top)
so ApplyAnchor early-returns and doesn't re-pin the window every frame.
Matches the vitalsRoot idiom exactly.

D2 — Cannot grab toolbar by chrome: toolbarRoot.ClickThrough = false
so HitTest succeeds over the UiDatElement chrome and the drag starts.
UiDatElement ctor defaults ClickThrough=true; vitalsRoot already overrides it.

C1 — All four combat-mode indicators visible at once (war/flame stacked on
peace): ports gmToolbarUI::RecvNotice_SetCombatMode
(acclient_2013_pseudo_c.txt:196632-196669). CombatIndicatorIds[] maps
index 0-3 to NonCombat/Melee/Missile/Magic; SetCombatMode shows exactly one
and hides the other three. Default to NonCombat at bind (player always
spawns in peace). Wires CombatState.CombatModeChanged for live updates.
Tests: CombatIndicator_defaultNonCombat_onlyPeaceVisible,
CombatIndicator_setCombatModeMelee_onlyMeleeVisible,
CombatIndicator_liveSignal_updatesWhenCombatStateChanges.

V1 — Blue empty-slot square at top-left (prototype 0x100001B2 materialized):
ImportInfos now skips top-level elements that are (a) referenced as a
BaseElement by another element in the same layout AND (b) have no own state
media. The CollectBaseRefsInDesc walk covers nested children; HasNoOwnMedia
re-uses ToInfo's media extraction. The Resolve path reads BaseElement from the
raw dat via dats.Get<LayoutDesc> — it never depends on the prototype being in
the built widget tree — so the skip is safe. Conformance tests (vitals, chat)
are unaffected (they exercise Build, not ImportInfos).
Test: BuildFromInfos_PrototypeSkipped_DerivedPresent_PrototypeAbsent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:03:07 +02:00
Erik
b3e5e8b0f7 fix(D.5.1): toolbar use-item gates on in-world + logs; store controller field (review)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:52:28 +02:00
Erik
3b6f293dc8 feat(D.5.1): mount the toolbar window under ACDREAM_RETAIL_UI
Wire IconComposer + ToolbarController.Bind + the LayoutDesc 0x21000016
import into the if (_options.RetailUi) block in GameWindow, mirroring
the vitals/chat pattern. Add UseItemByGuid helper (direct send, no
proximity gate) near the B.4b use-item path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:44:52 +02:00
Erik
e9a5248972 fix(D.5.1): dispose IconComposer + RenderSurface GL handles (review)
Two GL texture leaks plugged, both found in code review of 6e82807:

1. _handlesByRenderSurfaceId (pre-existing gap): populated by
   GetOrUploadRenderSurface for UI sprites but absent from Dispose's
   Phase 3 sweep. Added foreach/_gl.DeleteTexture/Clear in Dispose.

2. _adhocHandles (new): the public UploadRgba8(byte[],int,int,bool)
   wrapper used by IconComposer stored composited icon handles nowhere,
   so they leaked. Added _adhocHandles list; wrapper now appends the
   returned GL name before returning. Dispose sweeps + clears the list.
   Tracking is intentionally in the PUBLIC wrapper only — the private
   UploadRgba8(DecodedTexture,bool) is shared by all keyed-cache paths
   and tracking there would cause double-deletes.

No behavior change to icon rendering. No GL-context unit test added
(no context in test projects); correctness is by-inspection + green
suite (2598 passing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:18:20 +02:00
Erik
6e82807863 feat(D.5.1): IconComposer — CPU alpha-over icon composite + cache
Adds IconComposer (AcDream.App.UI) which mirrors retail IconData::RenderIcons
(decomp 407524): decodes each RenderSurface layer directly via SurfaceDecoder,
composites them bottom-to-top with Porter-Duff alpha-over, and uploads the
result to a GL texture via TextureCache. Composited handles are keyed by the
(iconId, underlayId, overlayId) tuple so each unique combo is uploaded once.

Adds a public TextureCache.UploadRgba8(byte[], int, int, bool) wrapper — a
thin shell around the existing private overload — so IconComposer can upload
its CPU-side composite without duplicating any GL state logic.

Pure Compose() path is covered by 2 unit tests (opaque top wins; transparent
top preserves bottom). Dat-decode + GL-upload exercised by the visual gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:09:41 +02:00
Erik
6c485c2f06 feat(D.5.1): persist PlayerDescription shortcuts (were parsed then discarded)
Add optional `onShortcuts` callback to `GameEventWiring.WireAll`; invoke
it with `parsed.Shortcuts` after the inventory/equipped loops in the
PlayerDescription handler.  `GameWindow` holds the list in a new
`Shortcuts` property (initialized to empty) so the toolbar (D.5.1 Task 5)
can read hotbar slots without keeping a parser reference.  Existing callers
compile unchanged — the parameter defaults to null.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 22:01:40 +02:00
Erik
5382d0a9d2 feat(D.5.1): thread CreateObject IconId into ItemRepository via spawn event
Added uint IconId = 0 (defaulted, last positional param) to the EntitySpawn
record so existing call sites outside WorldSession compile unchanged. The
WorldSession invoke now passes parsed.Value.IconId as the final arg.
OnLiveEntitySpawned calls Items.EnrichItem unconditionally — it's a no-op
for non-item spawns (players/NPCs/furniture aren't in the repo), so the call
is safe for every incoming CreateObject.

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