Commit graph

729 commits

Author SHA1 Message Date
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
Erik
37911ed510 merge: A7 lighting (Fix A point-light shape + Fix B per-object selection) into main
Brings the worktree branch claude/thirsty-goldberg-51bb9b into main:
- aa94ced  per-vertex Gouraud + faithful calc_point_light (wrap + norm)
- 4345e77  per-OBJECT point-light selection (minimize_object_lighting)

Auto-merged cleanly against the D.2b retail-UI line (only GameWindow.cs
overlapped, resolved by git). Merged tree builds green; 35/35 Core lighting
tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:50:22 +02:00
Erik
2284a376ae feat(D.2b): write-mode movement gate that preserves autorun
In chat write mode the keyboard belongs to the input — typing "swd" must not
walk the character — but AUTORUN must keep going (the user can chat while
running).

- InputDispatcher.IsActionHeld now returns false while WantCaptureKeyboard is
  set (a focused chat input), the polling-path twin of the existing gate on
  Fired actions. This SUPERSEDES the old per-frame OnUpdate early-return, which
  also killed autorun. Gating here instead lets the movement block keep running,
  so autorun — a separate latched bool ORed into Forward at the call site, not a
  polled key — survives. Test updated to encode the new contract.
- GameWindow: the movement suppress-guard reverts to ImGui-devtools-only (the
  retail write mode no longer early-returns); wires DefaultTextInput = the chat
  input (Tab/Enter activation) and Input.Keyboard for clipboard. Drops the
  one-shot UI-scale diagnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:24:19 +02:00
Erik
ebfeaff840 feat(D.2b): UI render infra — overlay layer, DrawFill, crisp text, write-mode focus
The retail-look render + focus primitives this chat pass builds on:

- TextRenderer: an OVERLAY layer (sprite/rect/text buckets flushed AFTER the
  normal layer) so an open popup composites on top of everything incl. rect
  panel backgrounds; a DrawFill primitive (solid quad via a 1x1 white texture)
  routed through the SPRITE bucket so a panel background draws UNDER its text
  instead of being washed by the later rect bucket; and the text pass now
  disables SampleAlphaToCoverage + Multisample so glyph alpha edges aren't
  dithered into MSAA coverage (the "fuzzy text") — self-contained GL state
  per feedback_render_self_contained_gl_state.
- UiRenderContext.DrawStringDat: snap the line baseline to a whole pixel ONCE
  then add the integer per-glyph offset (retail DrawCharacter takes an int
  pen-Y + schar m_VerticalOffsetBefore) — fixes the "letters dip down" jitter
  at a fractional line origin. Outline pass is now opt-in (retail gates it per
  element via SetOutline; default off = crisp fill-only). Adds DrawFill +
  Begin/EndOverlayLayer.
- UiElement: OnDrawOverlay + DrawOverlays (second traversal), FindRoot (blur
  self), ResetAnchorCapture (re-baseline an anchored element after reflow).
- UiRoot: runs the overlay pass after the main tree; Tab/Enter focuses the
  DefaultTextInput (write-mode activation); a left click on a non-edit target
  blurs the focused input (exit write mode without submitting).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 15:23:48 +02:00
Erik
828bec5fb5 @
fix(D.2b): point-sample the dat-font atlas so UI text is pixel-crisp

The font glyph atlas was uploaded with bilinear (Linear) min/mag filtering, which
softens the small dat-font glyphs (the menu/button text "blur"). Add a nearest-filter
path to UploadRgba8/GetOrUploadRenderSurface and use it for the font atlases only
(world + other UI surfaces keep bilinear). Combined with the existing per-glyph
pixel-snap, glyph texels now map 1:1 to screen pixels. Sharpens all dat-font text
(transcript, menu, Send/Chat buttons, vitals numbers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-16 12:02:07 +02:00
Erik
1da697ec2a @
feat(D.2b): chat polish — typing fix, opacity, scrollbar 3-slice, retail channel menu

Visual-iteration batch (decomp-grounded), each fix verified against the retail screenshots:
- typing: UiElement.HitTest aborted on ClickThrough BEFORE walking children, so the
  ClickThrough UiDatElement panels blocked hit-testing to the input/transcript inside
  them. Check ClickThrough AFTER the child walk (it only gates whether THIS element
  claims the hit). Restores input focus + typing.
- opacity: UiElement.Opacity + a UiRenderContext alpha stack applied to sprite/rect
  draws (text bypasses it, stays sharp); chat frame Opacity=0.75 → translucent chat.
- brown sliver: grow the transcript panel up 9px to cover the dropped resize-bar strip.
- scrollbar: real 3-slice thumb (caps 0x06004C60/66 + tiled mid) + tiled track.
- max/min: shifted one button-width left of the scrollbar (dat right-anchors collide).
- system text now green (retail ChatMessageType 5; was yellow).
- word-wrap: transcript lines wrap to the panel width (greedy, ports GlyphList::Recalculate).
- channel menu reworked to retail gmMainChatUI::InitTalkFocusMenu: "Chat" button + a
  TWO-COLUMN popup of the 14 talk-focus items (Squelch, Tell to Selected, Chat to All,
  Tell to Fellows, ...) on a tan panel; channel items set the active outbound channel.

Build + 392 App tests green. Visual confirmation in progress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-16 09:37:40 +02:00
Erik
0ec36f6197 fix(D.2b): chat input resolves the live command bus lazily (was bound to null) + register thumb-3-slice row
The live session + its LiveCommandBus are created after the retail-UI block in
OnLoad, so binding the bus by value captured NullCommandBus and silently dropped
outbound chat. Pass a Func<ICommandBus> resolved at submit time (mirrors how the
ImGui ChatPanel re-reads the bus each frame).

AP-41: scrollbar thumb drawn as single stretched tile (0x06004C63) instead of
retail's 3-slice top-cap/middle/bottom-cap — acknowledged in UiChatScrollbar.cs:37,
registered per the divergence-register rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:24:44 +02:00
Erik
12ab9663d2 feat(D.2b): cut GameWindow over to the data-driven chat window
Replace the hand-authored chat block (UiNineSlicePanel + inline UiChatView
+ local BuildRetailChatLines/RetailChatColor statics) with
ChatWindowController.Bind(LayoutDesc 0x21000006) — the same LayoutImporter
path as the vitals window.  The controller places UiChatView (transcript) +
UiChatInput (text entry, on-submit) + UiChatScrollbar + UiChannelMenu inside
the dat-authored chrome.  The dead local statics are deleted.

Wired to _commandBus (same LiveCommandBus as the ImGui ChatPanel) so
type+Enter dispatches SendChatCmd server-ward.  Transcript keyboard set from
_uiHost.Keyboard (set by WireKeyboard above the chat block) for Ctrl+C/Ctrl+A.

Divergence register: added AD-28 (two-widget split vs UIElement_Text),
AP-38 (no in-element word-wrap), AP-39 (per-line colour vs per-glyph runs),
AP-40 (no opacity fade / shared vitalsDatFont), TS-30 (tab buttons no-op),
TS-31 (no squelch); updated IA-15 to cover both vitals + chat importer paths.

Build: 0 errors/warnings.  Tests: 392 passed, 1 skipped (expected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:15:04 +02:00
Erik
4345e77d62 fix(render): A7 Fix B — per-OBJECT point-light selection (minimize_object_lighting)
Outdoor objects brightened as the camera approached: lighting selected the
nearest 8 lights to the VIEWER and fed that one global set to everything
(LightManager.Tick), so a building's wall torches only lit it once the camera
got close enough for them to win the global top-8. Probe confirmed the scale of
the problem: a single Holtburg view registers 129 point lights — the global cap
of 8 was hopeless.

Retail selects up to 8 lights PER OBJECT by the object's own position
(minimize_object_lighting 0x0054d480), so a torch always lights the wall it
sits on, camera-independent. Ported faithfully:

- LightManager.SelectForObject (pure, TDD, 8 new tests): candidacy
  (light.pos − center)² < (Range + radius)², nearest-8 among those. Plus
  BuildPointLightSnapshot for the per-frame stable-indexed light list.
- mesh_modern.vert: two SSBOs — binding=4 GLOBAL point-light array (the
  snapshot), binding=5 per-instance light SET (8 int indices into it, -1 =
  unused), parallel to the binding=0 instance buffer (mirrors the U.3 clip-slot
  mechanism). accumulateLights keeps ambient + sun from the SceneLighting UBO
  (cleared as faithful by the lighting audit) and loops THIS instance's point
  lights. pointContribution factored out (same calc_point_light wrap+norm shape).
- WbDrawDispatcher: per-entity light set computed ONCE at the isNewEntity site
  (constant across the entity's parts), by the entity's AABB sphere; threaded
  into grp.LightSets parallel to grp.Matrices; global + per-instance buffers
  uploaded in Phase 5. Camera-independent ⇒ stable for static buildings.
- GameWindow: BuildPointLightSnapshot + dispatcher.SetSceneLights each frame.

Tests: 17/17 LightManager + 36/36 dispatcher clip-slot/clip-frame green
(parallel-array lockstep preserved). Visually gated: the meeting hall now holds
steady as the camera approaches (was the popping symptom).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:47:40 +02:00
Erik
aa94cedc38 fix(render): A7 point-light shape — per-vertex Gouraud + faithful calc_point_light (wrap + norm)
The torch/point-light look was wrong two ways, both now fixed against the
named retail decomp (calc_point_light 0x0059c8b0) via our verified
LightBake.PointContribution port:

1. Per-PIXEL → per-VERTEX. accumulateLights moved from mesh_modern.frag to
   mesh_modern.vert so point lights Gouraud-interpolate across each triangle
   the way retail's fixed-function T&L does. The per-pixel eval made a tight,
   hard-edged "spotlight" pool on flat walls; per-vertex is a soft, broad
   gradient. frag now just consumes the interpolated vLit (+ fog + flash).

2. Simplified ramp → faithful calc_point_light shape. The live point/spot
   branch was max(0,N·L) × linear(1−d/range) × cap — missing two terms our
   LightBake.cs port already has:
     • half-Lambert WRAP (1/1.5)·(N·D + 0.5·d), D un-normalised — a face
       angled away from a torch still catches light (retail's soft terminator)
       instead of snapping to black.
     • distance-cube NORM branch norm = distsq>1 ? distsq·d : d — inverse-
       square-ish soft far halo + punchy near field, vs the flat linear ramp.
   Per-channel no-blowout cap (min(scale·color, color)) retained.

The per-channel cap was also added to the legacy mesh.frag for consistency.

A read-only retail-vs-acdream lighting audit (11-agent workflow) confirmed
these two as the cause of the "better but a bit off" look and cleared the
ambient/sun/terrain/color-space chain as already faithful. Remaining
confirmed divergences (per-object light selection; dungeon static vertex
bake) are filed as the next fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:27:27 +02:00
Erik
43064bab09 fix(D.2b): draw UI sprites in submission order so stamina/mana numbers render
TextRenderer batched sprites per-texture and drew each texture's whole buffer at
its FIRST-insertion point. The dat-font glyph atlas is one shared texture used by
all three vital numbers; it first appeared at the health bar, so all three numbers
were emitted right after the health bars — then the stamina + mana bar sprites
painted over their own numbers (only health survived). Replaced the per-texture
dictionary with submission-ordered segments (consecutive same-texture quads still
batch); each meter's number now draws after its own bars. The renderer's own
comment had predicted this break once bars became sprites (importer did that).
Removed the temporary UiMeter label diagnostic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:27:13 +02:00
Erik
8aa643f3e0 fix(D.2b): correct edge-anchor mapping (RightEdge==1=stretch) + enable vitals horizontal resize
ToAnchors was inverted vs retail UIElement::UpdateForParentSizeChange @0x00462640:
stretch is RightEdge==1 (not ==2/==4), LeftEdge==2 = track-right. Verified against
all 19 vitals fixture pieces. Enables Resizable/ResizeX on the importer vitals root
(the prior 'dat is fixed-size' conclusion was wrong). At-rest render unchanged
(anchors only fire on resize). Added a 160->200 resize conformance test.
Also fixed DatWidgetFactoryTests.RectAndAnchors_SetFromElementInfo which encoded
the old inverted model (Right=2 expecting Right anchor; corrected to Right=1).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:05:04 +02:00