Brainstorm + decomp investigation for the OPEN issue "Inventory + equipment
slots show the wrong empty-slot background art". Verified against the named
decomp that retail's UIElement_ItemList::InternalCreateItem (004e3570) sources
each list's empty-cell sprite from attribute 0x1000000e on the list's own
ElementDesc -> catalog LayoutDesc 0x21000037 -> the prototype's ItemSlot_Empty
(0x1000001c) media. acdream hardcodes 0x060074CF (the generic toolbar square),
bypassing the per-list cell-template inheritance entirely.
Design ports the resolver: a new ItemListCellTemplate.ResolveEmptySprite helper
(mirrors the existing GameWindow 0x21000037 digit-array read), a
UiItemList.CellEmptySprite property, and InventoryController wiring for the
contents grid / side-bag / main-pack lists. Toolbar untouched (its 0x060074CF
is correct); paperdoll silhouettes stay Sub-phase C.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6 tasks: (1) UiItemList clip+scroll via the shared UiScrollable + whole-row
clip; (2) mouse-wheel; (3) InventoryController binds the gutter scrollbar
0x100001C7 like ChatWindowController; (4) side-bag column 36px pitch + empty-
slot padding to capacity; (5) backdrop coverage — screenshot-gated, primary
fix is the clip; (6) verify + bookkeeping. TDD on the pure/controller logic
(internals test-visible); backdrop is the visual gate. Grounded in the dat
dumps + the existing scroll/scrollbar machinery.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stage 1 of the "full retail inventory" arc (Stage 2 = paperdoll, separate
spec). Grounded in the dat dumps: contents grid 0x100001C6 is 192x96 (6x3) +
scrollbar 0x100001C7 (16x96); backdrop 0x100001D0 is full-window 300x362
(so the "torn" look is the unclipped grid overflowing below the frame —
fixed by clipping); side-bag column 0x100001CA is 36x252 (7 slots).
Components: (A) UiItemList clip+scroll reusing UiScrollable + bind the gutter
scrollbar like ChatWindowController; (B) backdrop coverage — primary fix is A,
residual gated on the post-A screenshot; (C) side-bag column pads empty slots
up to capacity at 36px pitch. Paperdoll + B-Drag out of scope.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reword AP-48 (burden) and AP-49 (carry-aug) to fallback/default-only: B-Wire
ports the retail wire read (login PD-bundle UpsertProperties + live 0x02CD;
ObjectTableWiring applies all ints), so SumCarriedBurden / aug=0 are now
defensive only. Confirm the server sends EncumbranceVal/0xE6 at the visual
gate, then delete the rows. Add the D.2b-B B-Wire SHIPPED entry to ISSUES.
(Memory digest project_d2b_retail_ui.md + MEMORY.md updated out-of-tree.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Design/spec for B-Wire (follows B-Controller c38f098). Root cause found:
the burden binding already reads wire EncumbranceVal (PropertyInt 5) but
the value never arrives — login PD parses the player's int table then
drops it, the live 0x02CD (PrivateUpdatePropertyInt, no guid) is unparsed,
and ObjectTableWiring gates all non-UiEffects ints out.
Scope (user chose the full wire pass): player-property delivery (login PD
bundle upsert + 0x02CD parse + loosen the apply gate, retiring AP-48/AP-49),
latent-bug fixes (0x0022 4th field, 0x00A0 error), new C→S builders
(DropItem 0x001B / GetAndWieldItem 0x001A / NoLongerViewingContents 0x0195),
new S→C parsers (ViewContents 0x0196 GameEvent; SetStackSize 0x0197 +
InventoryRemoveObject 0x0024 GameMessages), and WireAll registration.
Opcodes pinned against ACE GameMessageOpcode.cs / GameActionType.cs; every
format requires grep-named → cross-ref → pseudocode → port + conformance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- #145: the original fix was incomplete (mounted panels inherited ZLevel 1000 →
behind the backdrop → washed out); continuation 417b137 closes it. Updated status.
- D.2b-B SHIPPED entry: visually confirmed; the two render bugs (backdrop wash-out
+ captions host-direct) documented.
- New OPEN issue: contents grid overflows (needs the gutter scrollbar wired; scroll
was out-of-scope for B-Controller per the spec).
- Roadmap ledger: B-Controller visually confirmed + the render fix noted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opus phase-boundary review findings:
- I1 (faithfulness): LoadToPercent now computes from the CLAMPED fill
(floor(LoadToFill(load)*300)), so the burden % SATURATES at 300% like retail
(decomp 176544-176576 clamps arg2 to [0,1] BEFORE the *300). The old
floor(load*100) over-read to 400% at 4x capacity. Golden test corrected.
- I2 (partition): exclude equipped items (CurrentlyEquippedLocation != None) from
the contents grid + selector — a mid-session self-wield routes them through
MoveItem(item, WielderGuid=player) into GetContents(player); retail's gm3DItemsUI
shows pack contents only. New conformance test.
- N1: drop dead 'using System.Collections.Generic' (left after the 383e8b7 cleanup).
- N3: AP-48 risk wording (drift can be low OR high vs server EncumbranceVal).
Build green; BurdenMath 17, InventoryController/UiMeter 10 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read-only population of the gmInventoryUI tree: bind 0x21000023 by id,
fill the 'Contents of Backpack' grid + the right-strip pack-selector from
ClientObjectTable, drive the vertical burden meter, render the Type-0
captions. Faithful selector + full faithful burden meter (per brainstorm).
Ported AC algorithms with decomp anchors: CACQualities::InqLoad (0x0058f130),
EncumbranceSystem::EncumbranceCapacity (0x004fcc00) / Load (0x004fcc40),
gmBackpackUI::SetLoadLevel (0x004a6ea0), UIElement_Meter::DrawChildren
(0x0046fbd0) / Initialize (0x0046f7b0, m_eDirection from property 0x6f).
Key finding: BuildMeter's single-image sprite assignment is already correct
(meter-own=track, child=fill); only vertical fill is new.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
B-Grid sub-window mount works (paperdoll base 0x100001D4 has 25+ equip-slot
children, attached), but the gmInventoryUI panels render blank: the
importer maps ZOrder from ReadOrder only, so the full-window backdrop
(0x100001D0, ReadOrder 4) paints over the panels (ReadOrder 1-3). Retail
keeps it behind via ZLevel (backdrop 100 vs panels 0), which the importer
ignores (vitals were all ZLevel 0). Fix scoped in the issue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4-task TDD plan: UiItemList grid mode (CellOffset/Columns/cell pitch),
the LayoutImporter.Resolve sub-window mount (ShouldMountBaseChildren +
attach base subtree), GameWindow swap of the placeholder for the real
Import(0x21000023), then full-suite regression guard + visual.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First step of Sub-phase B (inventory window). Dumped LayoutDesc 0x21000023:
the three nested panels (paperdoll/backpack/3D-items) are Type-0 pure-
container leaves that nest via the EXISTING BaseElement+BaseLayoutId
inheritance path (BaseLayoutId → 0x21000024/22/21), NOT game-class Types as
the research agent claimed. ElementReader.Merge drops base children, so they
import empty today. Mount = a surgical ~4-line LayoutImporter.Resolve change:
a childless, media-less inheritor attaches its base's resolved subtree
(predicate ShouldMountBaseChildren, unit-testable; inert for media-bearing
inheritors + childless style prototypes, so vitals/chat/toolbar are
unaffected). Plus UiItemList N-cell grid mode (single-cell toolbar preserved).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The UiRoot window manager (RegisterWindow/Show/Hide/Toggle/BringToFront)
is more of the same IA-12 toolkit-reimplementation of keystone semantics,
so per the dedup convention it joins IA-12's Where as a secondary site
rather than getting a redundant row. F12 inventory toggle is retail-
faithful (no divergence).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4-task TDD plan for Sub-phase A: UiRoot named-window registry
(Show/Hide/Toggle/BringToFront) + raise-on-click + F12 wiring of the
existing ToggleInventoryPanel action to a throwaway placeholder inventory
window that Sub-phase B replaces. Divergence handled by extending IA-12.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sub-phase A of the window-manager → inventory → paperdoll arc. A named-
window registry on UiRoot (Show/Hide/Toggle/BringToFront) + raise-on-click
+ wiring the EXISTING InputAction.ToggleInventoryPanel (already F12-bound,
retail-faithful) through OnInputAction, toggling a throwaway placeholder
inventory window that Sub-phase B replaces with the real gmInventoryUI.
Cross-check correction vs the handoff: retail's inventory key is F12, not
I (I = Laugh emote per retail-default.keymap.txt:246). Reuses the existing
action, so no new keybind — rebindable via the Settings panel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Frames the next core-panels work for a fresh session, after B.1 (drag spine) +
B.2 (toolbar reorder/remove + wire) + toolbar collapse merged this session.
Three sub-phases in order: (A) minimal window manager (open/close + I-key toggle;
UiHost has no open/close API today, windows are always-on); (B) inventory window
(Stream C, synthesis §4 — UiItemList grid + sub-window mount + wire gaps +
InventoryController; the drag SOURCE that closes B.2's drag-from-inventory via a
SourceKind==Inventory branch in ToolbarController.HandleDropRelease); (C) paperdoll
UiViewport (Type 0xD) + Core→App IUiViewportRenderer seam, heaviest, last. Spell
bar deferred. Reuse the shipped spine/ShortcutStore/wire. Paste-able new-session
prompt + MEMORY index line inside.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
User request: the toolbar frame resizes vertically between one row (row 2
hidden, minimum) and two rows (shown), SNAPPING between the two stops; default
expanded. Small toolkit feature: UiElement.MaxHeight + a ResizableEdges mask
(bottom-edge-only) + a UiCollapsibleFrame (snaps height to the nearer stop and
ties row-2 visibility to it in OnTick) + the GameWindow mount (compute the two
heights from the layout, top-anchor the content so row 1 never reflows). Retail's
real mechanism is keystone.dll (no decomp) + the dat stacks both rows always —
so this is a toolkit UX from the user's retail observation; amends IA-17.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The final B.2 review found the TS header undercounted: there are 32 contiguous
rows (TS-1..TS-32) but the header read "31". The base already undercounted (said
"32" with 33 rows TS-1..TS-33); deleting TS-33 with a literal 32→31 decrement
preserved the off-by-one. Set the header to the verified actual count.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Bite-sized TDD plan, three slices: (1) Core ShortcutStore + the AddShortcut
0x019C/RemoveShortcut 0x019D wire (rename BuildAddShortcut to the real
Index/ObjectId/SpellId/Layer fields; SendAddShortcut/SendRemoveShortcut); (2)
spine extensions (OnDragLift hook; ghost snapshotted at BeginDrag at full
opacity; FinishDrag delivers a drop only on a real hit); (3) ToolbarController
as the live handler (store-driven Populate w/ lazy-load; green-cross FA overlay;
OnDragLift removes + HandleDropRelease places + bumps displaced→source; wire
actions injected from GameWindow). Amends AP-47, deletes TS-33. Spec + plan
preapproved; executing subagent-driven next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From the user's visual-gate feedback + a retail decomp trace this session.
Retail's model is remove-on-lift / place-on-drop / no-restore (confirmed:
RecvNotice_ItemListBeginDrag 0x004bd930 → RemoveShortcut 0x004bd450 at lift;
HandleDropRelease 0x004be7c0 places + bumps displaced → source; off-bar drop
leaves it removed). This unifies the user's points 3/4/5 into one mechanism.
Verified the drop sprite against client_portal.dat: 0x060011F9 = green RING
(inventory), 0x060011FA = green CROSS (toolbar) — user was right.
Scope (toolbar-internal; drag-from-inventory is Stream C): spine extensions
(lift hook on IItemListDragHandler; ghost snapshotted at BeginDrag at full
opacity so it survives the source emptying; FinishDrag delivers a drop only on
a real hit, off-bar = nothing); a mutable 18-slot ShortcutStore; ToolbarController
as the live handler (OnDragLift removes; HandleDropRelease places + bumps); the
AddShortcut 0x019C / RemoveShortcut 0x019D wire (fix the BuildAddShortcut param
names; SendAddShortcut/SendRemoveShortcut on WorldSession); green-cross overlay.
Amends AP-47 (ghost now full opacity), retires TS-33 (real wire replaces the stub).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Filed from the B.1 spine code review. No-op today (toolbar Clicked guards
ItemId!=0); deliberately NOT guarded with a speculative _dragCancelled bit
(would make item-slots differ from every other widget + guess at retail).
Verify retail's empty-cell press+move+release before changing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements Task 4 of the drag-drop spine: ToolbarController now satisfies
IItemListDragHandler. The ctor loop wires RegisterDragHandler(this) on every
slot list and stamps Cell.SlotIndex + Cell.SourceKind=ShortcutBar, matching
retail's gmToolbarUI::PostInit/RegisterItemListDragHandler pattern. OnDragOver
accepts any non-empty payload (TS-33 stub; eligibility gate is Stream B.2).
HandleDropRelease logs and returns (no AddShortcut 0x019C / RemoveShortcut
0x019D wire yet). Three new B.1 xUnit tests cover handler registration, accept,
and inert-stub semantics. All 490 app tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stream B.1 of the 2026-06-18 handoff — the shared widget-level drag-drop
infra that both shortcut-drag (B) and the inventory window (C) sit on.
Four design decisions confirmed with the user this session:
1. Payload = typed ItemDragPayload record snapshotted at drag-begin
(ObjId/SourceKind/SourceSlot/SourceCell); SourceContainer derived at
drop from ClientObjectTable (single source of truth).
2. Cursor ghost painted by UiRoot via a generic UiElement.GetDragGhost()
hook — keeps UiRoot item-agnostic; floats above all windows.
3. Cell (UiItemSlot) is the drop-target hit unit + accept/reject overlay
owner, delegating the decision + dispatch UP to its parent UiItemList's
registered IItemListDragHandler (faithful to retail cell->ItemList_DragOver
->m_dragHandler; scales to the inventory N-cell grid).
4. PR ships infra + a visible toolbar STUB handler (logs, no wire) so the
ghost/overlay/dispatch are confirmable this session; AddShortcut/Remove
wire is Stream B.2.
Retail-grounded: InqDropIconInfo flags (&0xE==0 fresh / &4 reorder, reject
state 0x10000040) confirmed live at gmToolbarUI 0x004bd162; the cell
begin-drag/CatchDroppedItem/RegisterItemListDragHandler chain at decomp
229344/229744/230461. Planned register rows: AP-47 (ghost reuses full icon
at reduced alpha vs retail m_pDragIcon) + TS-33 (toolbar drop stub pending
B.2). No new wire format in the spine itself.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clean handoff for the next M1.5 "indoor world feels right" session, picking up
the two indoor-lighting gaps the user spotted at the #140 visual gate.
#142 (PRIMARY): windowed-building interiors + look-ins read "like outdoors".
Root cause grounded: retail's lighting regime is per-DRAW-STAGE (PView::DrawCells
draws ALL EnvCells in the useSunlightSet(0) interior stage — torch-lit, no sun,
regardless of SeenOutside), while acdream's is a per-FRAME global keyed on the
player's cell (playerInsideCell). So acdream's windowed interiors (SeenOutside)
+ look-ins stay in the outdoor regime. This is the AP-43 residual surfaced.
Fix direction: make sun+ambient per-draw like AP-43's torches (design fork laid
out for a brainstorm). Resolves AP-43.
#143 (SECONDARY): portal swirl casts no light. acdream registers only static
Setup.Lights; the portal is a retail DYNAMIC light (add_dynamic_light ->
minimize_envcell_lighting). Fix: register a dynamic LightSource for portals.
Handoff doc carries the verified retail decomp (useSunlightSet/PView::DrawCells
stages), current acdream line refs, the three gaps, the fix fork, validation
plan, and DO-NOT-RETRY. Neither issue is a regression from #140.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Selected-object meter health half passed the visual gate (2026-06-20): name on
the black band, attackable-only health gate, UpdateHealth-driven bar, green flash,
no magenta. Mana (0x100001A2) + stack entry/slider (0x100001A3/A4) remain deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
Same-instant cdb proved acdream ambient (0.447) == retail (0.4465) and time/sun match,
so the building/character over-brightness is NOT the bake/wrap/EnvCell/clamp (D-1..D-4,
all correct but off-target) — those light the wrong surfaces. The Holtburg building
exterior is a mode-0 OBJECT (IsBuildingShell, not an EnvCell). Isolation (object point
lights gated OFF) made it match retail => cause is the torch REACH being too long
(acdream range 7.8 = Falloff 6x1.3 vs retail 5.2 = Falloff 4x1.3), flooding the small
facade. OPEN: confirm same-torch Falloff acdream-vs-retail before tightening the reach.
Diagnostic shader hack reverted (tree clean); D-1..D-4 kept. Branch not merged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Brainstormed design for the action bar's bottom strip: name + Health meter
on selection (mana deferred #140). Decisions: SelectionChanged via property
setter; send QueryHealth(0x01BF) on select. Grounded in retail
gmToolbarUI::HandleSelectionChanged (acclient_2013_pseudo_c.txt:198635) —
clear-then-populate, overlay state 0x1000000b, health gate
IsPlayer||pet||attackable. Render-bug fix is BuildMeter-only (single-image
back+fill meter; UiMeter already renders it).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Next D.2b-UI work after D.5.4. 3 streams (spell bar deferred): selected-object
meter, shortcut drag/add/reorder/remove, inventory+paperdoll window. Current-code
anchors + dependency graph + build order + brainstorm questions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AP-35: the "numerically equivalent" claim was false. Residual is now two
parts: (a) per-frame GPU evaluate vs retail's bake-once (architecture/perf
difference only; formula matches), and (b) SelectForObject 8-cap means a
surface reached by >8 point lights is dimmer than retail's uncapped bake.
Cross-references AP-16 for the cap ownership.
AP-16: the old "global nearest-8 viewer-distance into UBO" description was
stale — the UBO point-light path is now vestigial (mesh_modern.vert skips
posAndKind.w!=0 entries; point lights come exclusively from the per-object
SSBO binding 5). Retargeted to the current SelectForObject per-object/cell
8-cap mechanism with correct file:line (LightManager.cs:234), both call
sites (ComputeEntityLightSet + GetCellLightSet), and the retail oracle
distinction (hardware cap 0x0054d480 faithful; bake 0x0059cfe0 not).
Preserved the UBO-directional-only note inline rather than losing it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix A (aa94ced) moved point lighting to per-vertex Gouraud and ported the
half-Lambert wrap + norm distance attenuation. Fix D D-1 added the separate
point-light accumulator clamped to [0,1] matching retail's
SetStaticLightingVertexColors bake clamp.
AP-35 previously stated the path was per-pixel (mesh_modern.frag:52) and
that wrap + normalization factor were "neither ported" — both wrong. Rewrite
to reflect current state: per-vertex in mesh_modern.vert (pointContribution),
wrap + norm ported, point sum clamped. Residual is architecture-only (per-
frame GPU evaluate vs retail bake-once), not a visual divergence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task-by-task TDD plan: (1) extract GlobalLightPacker (Core, pure) + test + refactor
WbDrawDispatcher; (2) lock the bake contract via LightBake conformance test on the
captured golden torches; (3) D-1 clamp the point-light sum on its own in
mesh_modern.vert; (4) D-2 EnvCellRenderer binds its own per-cell light set (SSBO 4+5)
via SelectForObject over cell bounds; (5) correct register AP-35 + reconcile Fix B.
Concrete code + exact insertion points; visual verification is the acceptance gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve the Fix D contradiction with decomp (workflow wf_f660eb88 + adversarial
verify) + 4 live cdb captures. The D3D-FF model was the WRONG oracle: retail has
TWO light systems — STATIC torches BAKE into wall vertices (calc_point_light,
triple-clamped: range gate + per-channel min(scale*color,color) + per-vertex
[0,1] from black), DYNAMIC lights go D3D hardware. The captured intensity=100 is
the purple PORTAL (magenta, dynamic), not a wall torch. Ground truth: 38 static
warm torches (orange (1,0.588,0.314)/cream, intensity=100, falloff 3-5) + 2 dynamic.
acdream over-brightness = two confirmed bugs: D-1 mesh_modern.vert folds
ambient+sun+torches into one UNCLAMPED accumulator (single frag clamp) -> warm
blowout; D-2 EnvCellRenderer never binds SSBO 4/5 so the cell shell reads a leaked
light set. Spec: D-1 in-shader clamp-split (clamp the torch sum on its own before
ambient/sun); D-2 bind the shell's own per-cell light set (mirror WbDrawDispatcher);
LightBake.cs is the C# conformance oracle. Adds the 4 reusable cdb capture scripts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code-review follow-up from Task 2: align StackSizeMax with the other quantity
fields (int?, ACE PropertyInt convention) in Tasks 3/4/5; drop the (int) cast.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>