Commit graph

614 commits

Author SHA1 Message Date
Erik
d400bc6105 docs: handoff — finish the action bar (selected-object meter + shortcut drag) + start the inventory/paperdoll window
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>
2026-06-18 21:29:04 +02:00
Erik
85a2371e11 docs(D.5.4): roadmap shipped + divergence register (event model + deferred parent pre-queue)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:03:28 +02:00
Erik
e4dd37a3b8 docs(D.5.4): plan — StackSizeMax int? for downstream type consistency
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>
2026-06-18 15:51:29 +02:00
Erik
2fc253d9ff docs(D.5.4): implementation plan (12 tasks, TDD, green-per-task)
Rename -> field capture -> EntitySpawn plumb -> ClientObject/WeenieData ->
Ingest merge-upsert -> container index -> ObjectTableWiring (off GameWindow) ->
PD manifest -> delete EnrichItem -> retire _liveEntityInfoByGuid -> toolbar
guid-filter -> bookkeeping+live run. Sequenced so every task builds green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:19:15 +02:00
Erik
969e55350b docs(D.5.4): client object/item data model design (brainstorm spec)
Two guid-keyed tables (retail shape), CreateObject = canonical merge-upsert
for the data table (ACCWeenieObject-equivalent holding ALL server objects),
container membership index, retire _liveEntityInfoByGuid + EnrichItem. Settles
the handoff crux against the named decomp: retail is TWO tables, not one, so
acdream's WorldEntity + item-table split is already faithful — fix ingestion,
don't unify.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 15:06:00 +02:00
Erik
5b568d000a docs(D.5): sub-phase ledger + item-model cold-start prompt
Roadmap: refresh the D.5.2 entry to its final shipped state (per-pixel gradient
surface overload 0x004415b0; AP-43/AP-44 retired by visual verification; range
419c3ac..fb288ad). Add an explicit D.5 sub-phase ledger: D.5.4 client object/item
data model (foundation, NEXT) -> D.5.3 selected-object + spell shortcuts -> window
manager -> D.5.5+ core panels. Handoff doc gains a paste-ready new-session prompt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:41:08 +02:00
Erik
9e0d2568cc docs: handoff for the client object/item data model (next phase after D.5.2)
Frames the root cause of the live-Coldeve 4/6-missing-hotbar-icons (acdream's
enrich-existing-only item model drops CreateObjects without a pre-seeded stub) and
the retail ClientObjMaintSystem model to port. CRUX to settle first: unify the
WorldEntity + ItemRepository tracks, or keep separate with shared ingestion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:33:31 +02:00
Erik
fb288ad852 fix(D.5.2): effect tint = per-pixel tile copy (surface ReplaceColor overload)
Visual verification (Coldeve, Energy Crystal) showed acdream's Magical blue as a
flat tint vs retail's gradient. Root cause: RenderIcons calls the SURFACE overload
of SurfaceWindow::ReplaceColor (0x004415b0), which copies the textured effect tile
pixel-by-pixel into the icon's pure-white pixels — not the flat color->color overload
(0x00441530) I'd approximated with the tile's mean color. Port the surface overload
exactly (dst[x,y]=src[x,y] where dst==white); confirmed via clean Ghidra decompile +
named decomp. Retires AP-43 (mean-color approximation); IA-18 updated to the surface op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:21:33 +02:00
Erik
40c97a53ac fix(D.5.2): always run effect recolor (effects==0 -> black) to match retail
Visual verification caught it: a no-mana scroll's icon edges are BLACK in retail
but rendered WHITE in acdream. Cause = the effects!=0 gate (registered AP-44) that
skipped retail's effects==0 recolor. Retail's effect tile is non-null even for
effects==0 (the 0x21 SOLID-BLACK fallback 0x060011C5), so RenderIcons recolors
pure-white pixels to black on mundane items and to the effect hue on magical ones.
Remove the gate (always recolor); retire AP-44 (now faithful). TryGetEffectColor
made internal + a golden test pins effects==0 -> ~black.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 22:54:15 +02:00
Erik
73adc3768c docs(D.5.2): retire IA-16, add IA-18/AP-43..45, roadmap + memory
Divergence register:
- Retire IA-16 (item-icon composite PARTIAL — D.5.2 now complete).
- Add IA-18 (effect overlay = ReplaceColor tint SOURCE, faithful retail
  behavior; anti-regression guard — do NOT re-implement as a blit layer;
  cites IconData::RenderIcons 0x0058d180 + ReplaceColor 0x00441530).
- Add AP-43 (effect tint = mean-opaque color; exact retail byte
  decompiler-ambiguous, visual/cdb confirmation pending).
- Add AP-44 (effects==0 black-fallback recolor skipped; regression-risk
  avoidance, pending visual/cdb confirm).
- Add AP-45 (0x02CE sequence byte not honored, latest-wins).
Section header counts updated: IA 15→17, AP 41→44.

Roadmap: mark D.5.2 shipped (419c3ac..2f789da; appraise dropped as no-op;
effect recolor + live 0x02CE).

Tests: update ToolbarControllerTests iconIds lambda arity 4→5 to match the
D.5.2 GetIcon signature change (was caught by the build).

Memory: project_d2b_retail_ui.md updated with D.5.2 shipped entry
(via claude-memory symlink to ~/.claude/projects/.../memory/).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:52:15 +02:00
Erik
52306d9268 docs(D.5.2): implementation plan (9 TDD tasks) + spec wiring fix
Bite-sized TDD plan for the stateful item-icon system. Corrects spec 5.8:
the live 0x02CE event binds in GameWindow (next to VitalUpdated), not
GameEventWiring (which only handles the 0xF7B0 GameEvent dispatcher).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:19:26 +02:00
Erik
419c3ac40c docs(D.5.2): stateful item-icon spec + RESOLVED research
Research basis (clean Ghidra decompile via MCP + live-dat probe + ACE oracle)
overturns two handoff hypotheses:
  - Appraise carries NO icon/UiEffects data (Icon/IconOverlay/IconUnderlay +
    PropertyInt.UiEffects all lack [AssessmentProperty]); every icon input is
    CreateObject-only. The "wire appraise -> enrichment" item is a no-op.
  - The effect overlay (enum 0x10000005) is a ReplaceColor tint SOURCE, not a
    blit layer (RenderIcons 0x0058d180 + ReplaceColor 0x00441530); effect tiles
    are 32x32 fully-opaque colored squares.

Design (user-approved): capture UiEffects (weenieFlags 0x80, currently discarded)
-> ItemInstance.Effects; faithful 2-stage IconComposer recolor (white pixels ->
effect hue); live PublicUpdatePropertyInt(0x02CE) wire-up so the icon updates as
state changes ("item with mana vs out of mana"). Drops the appraise no-op.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 18:12:45 +02:00
Erik
6770381fc3 docs(D.5.2): stateful icon-system handoff + roadmap (D.5.1 shipped, D.5.2/D.5.3 next)
Extensive handoff for the next session to build the full stateful item-icon system (the 5-layer IconData::RenderIcons composite + effect layer 0x10000005 + overlay ReplaceColor tint + appraise-driven enrichment/re-composition). D.5.1 toolbar flipped to SHIPPED; D.5.2 (icon system) + D.5.3 (toolbar interactivity / selected-object display) registered as next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:24:03 +02:00
Erik
b1e45bee1c docs(D.5.1): divergence rows IA-16/IA-17 + ISSUES toolbar-interactivity entry
IA-16: partial icon composite (layers 1-4 only; effect glow + ReplaceColor tint
deferred to D.5.2). IA-17: per-window UiNineSlicePanel chrome vs window-manager-
owned bevel (retail toolbar has no baked frame in LayoutDesc 0x21000016).

#140: toolbar interactivity (selected-object meters 0x100001A1/A2 + stack slider
0x100001A4 + name line) deferred to roadmap D.5.3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:21:19 +02:00
Erik
30b28c248a docs(D.5.1): register toolbar phase-1 in the roadmap 2026-06-16 21:40:46 +02:00
Erik
44fabd350e docs(D.5.1): toolbar phase-1 implementation plan (+ spec wiring-delta note)
12-task TDD plan: register D.5.1 -> CreateObject IconId capture -> ItemRepository.EnrichItem -> spawn-event icon wiring -> persist shortcuts -> IconComposer (CPU composite) -> UiItemSlot -> UiItemList + factory branch -> ToolbarController -> GameWindow mount -> visual gate -> bookkeeping. Concrete call sites pinned (WorldSession.cs:701 EntitySpawned, GameEventWiring.WireAll, GameWindow Items@598, BuildUse 0x0036). Synced the spec's CreateObject section with the wider-than-expected wiring found during planning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:27:49 +02:00
Erik
0b5e849325 docs(D.5.1): toolbar phase-1 design spec
First D.5 sub-phase: ship gmToolbarUI as the first data-driven game panel (18 slots from LayoutDesc 0x21000016, populated from the persisted PlayerDescription shortcuts, real composited icons, click-to-use). Minimal scope; faithful CPU icon pre-composite (IconData::RenderIcons port). Five bounded units: UiItemSlot, UiItemList, IconComposer, CreateObject IconId extension, ToolbarController. Roadmap registration of D.5.1 is plan step 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:05:04 +02:00
Erik
a5c5126e8d docs(D.5): action bar / inventory / paperdoll research drop
Five report-only deep-dives + synthesis for the next D.2b UI panels, built on the shipped widget toolkit. Confirms LayoutDesc ids (toolbar 0x21000016, inventory 0x21000023, backpack 0x21000022, paperdoll 0x21000024, 3ditems 0x21000021), the shared item-slot/item-list spine (UIElement_UIItem 0x10000032 / UIElement_ItemList 0x10000031), the 5-layer icon composite (IconData::RenderIcons @407524), the cross-panel wire catalog with acdream parse-status, and the dependency-ordered build plan.

Produced via a multi-agent research workflow; the spine agent died on a transient API error and was re-run as a focused follow-up with its decomp anchors verified against source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 21:04:57 +02:00
Erik
78c91875b8 docs: file #139 — D.2b retail UI polish (chat text colors + buttons)
Deferred cosmetic polish after the widget-generalization landing: tune the
per-ChatKind transcript text colors against retail, and add pressed/hover state
feedback to the chat buttons (UiButton draws only its default state today; the
dat carries Normal/Pressed/Highlight). Not a regression — the generalized chat
matches the prior hand-made build (user-confirmed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 19:01:50 +02:00
Erik
9e4faae9d2 docs(D.2b): roadmap — widget generalization (Plan 2) shipped
Record the D.2b widget-generalization landing: generic Type-registered widgets
built by DatWidgetFactory, thin find-by-id controllers, the ConsumesDatChildren
leaf rule, Type-3-not-registered decision, and the centered-UiText vitals numbers.
Both visual gates user-confirmed; 404 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:55:06 +02:00
Erik
89626cd400 feat(D.2b): vitals numbers as UiText (widget-generalization Task 8)
The vitals cur/max numbers now render through the generic UiText widget — retail
gmVitalsUI uses UIElement_Text for them, not a meter-internal label. VitalsController
attaches a centered, non-interactive UiText child to each meter and stops the meter
drawing its own label (UiMeter.Label -> null). New UiText.Centered draws the first line
centered H+V with the SAME formula UiMeter's overlay used, so the numbers are
pixel-identical — user-confirmed in the live client.

This completes the D.2b widget-generalization pass: every chat + vitals widget is now
built generically and registered to its retail Type (Button/Field*/Menu/Meter/Scrollbar/
Text), with thin find-by-id controllers. (*Field is controller-placed; Type 3 stays
UiDatElement for chrome.)

Divergence register: AP-37 vitals-numbers-via-UiMeter.Label clause retired. Full suite:
404 passed, 2 skipped, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:52:42 +02:00
Erik
83076cdbb6 docs(D.2b): spec correction — input is Variant B, Type 3 not registered
Record the two execution-time corrections to the design's registration
assumptions: the editable input resolves to Type 12 (Variant B, controller-placed
UiField), and Type 3 is NOT factory-registered (acdream's Type-3 elements are
chrome/containers, kept on the UiDatElement fallback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:54:52 +02:00
Erik
cb082b59e4 feat(D.2b): UiText (Type 12) -- generic text + Type-12 flip; transcript factory-built (widget-generalization Task 5)
Rename UiChatView -> UiText (the retail UIElement_Text class,
RegisterElementClass(0xc) @ acclient_2013_pseudo_c.txt:115655).

Factory changes (DatWidgetFactory.cs):
- Remove the Type-12 skip (was: no-media -> null, with-media -> UiDatElement).
- Add Type 12 -> BuildText() -> UiText in the switch.
- BuildText extracts the element's Direct/Normal sprite as BackgroundSprite
  so any dat-media the element carried keeps rendering under the text.

UiText changes (renamed from UiChatView.cs):
- BackgroundColor default: (0,0,0,0.35) -> (0,0,0,0) (transparent).
  An unbound UiText draws nothing; the controller opts in to the translucent bg.
- New BackgroundSprite + SpriteResolve: optional dat state-sprite background
  drawn UNDER DrawFill+text (faithful UIElement_Text media support).

ChatWindowController.cs (Task 5 Step 8):
- Transcript property: UiChatView -> UiText.
- Bind() now uses layout.FindElement(TranscriptId) as UiText (factory-built)
  instead of manually constructing + AddChild-ing a new UiChatView.
- Sets BackgroundColor = (0,0,0,0.35) on the found widget (retail translucent bg).
- Removes the tInfo null-check from the early guard (transcript is factory-built;
  iInfo lookup kept for the input widget which is still manually constructed).
- BuildLines: UiChatView.Line -> UiText.Line throughout.

Vitals frozen: the Type-12 vitals number elements are meter children and are
never recursed by BuildWidget (the `if (w is not UiMeter)` gate), so they are
not built as widgets and keep rendering via UiMeter.Label. Vitals fixture
vitals_2100006C.json unchanged; LayoutConformanceTests + VitalsBindingTests green.

Tests:
- UiChatViewTests.cs -> UiTextTests.cs (class: UiTextTests, all UiChatView.* -> UiText.*)
- UiChatViewDatFontTests.cs -> UiTextDatFontTests.cs (same)
- DatWidgetFactoryTests: delete Type12_StylePrototype_ReturnsNull +
  DatWidgetFactory_Type12WithMedia_Renders; add Type12_Text_MakesUiText +
  DatWidgetFactory_Type12_AlwaysMakesUiText.
- LayoutImporterTests: BuildFromInfos_Type12Child_IsSkipped_Type3Present updated
  to assert IsType<UiText> (element is now in tree, transparent, not skipped).

Divergence register: AP-37 amended -- removed the "standalone Type-0 text
elements skipped / dat-text widget is Plan 2" clause (now shipped as UiText);
kept the meter-collapse clause and the vitals-numbers-via-UiMeter.Label clause.
AP-38/AP-39/AD-28 file references updated UiChatView.cs -> UiText.cs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:39:02 +02:00
Erik
955f7a69a8 feat(D.2b): UiMenu (Type 6) — generic dropdown; channel knowledge moves to controller (widget-generalization Task 4)
UiChannelMenu → UiMenu: removed ChatChannelKind, the 14-item array, the
button-text map, and the availability default. Generic surface: MenuItem
(label + object? Payload), Selected (object?), OnSelect, EnabledProvider,
ButtonLabelProvider, RowsPerColumn/RowHeight/ColumnWidth (all settable).
All draw/event mechanics unchanged — same popup geometry, same click
coordinates, same 8-piece bevel, same 3-slice button face.

ChatWindowController gains ChannelItems[], ChannelButtonLabel(), and
ChannelAvailable() (verbatim from old widget), and populates the
factory-built Type-6 UiMenu via find-by-id rather than constructing a
replacement widget. The Menu property type is now UiMenu. OnChannelChanged
wrap replaced with the generic OnSelect wrap for the ReflowInputRow hook.

DatWidgetFactory registers Type 6 → new UiMenu().

Tests: UiChannelMenuTests → UiMenuTests (10 tests, all green); factory
Type6 test added; ChatWindowControllerTests updated to use OnSelect.
Divergence register: AP-42 added (flat item model vs retail nested-submenu
MakePopup @0x46d310 — latent, unreachable through the chat menu).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:18:27 +02:00
Erik
3593d6623d feat(D.2b): UiScrollbar (Type 11) — promote the generic chat scrollbar (widget-generalization Task 2)
- git mv UiChatScrollbar.cs → UiScrollbar.cs; rename class + update doc summary to
  "Generic scrollbar. Ports retail UIElement_Scrollbar (RegisterElementClass(0xb) @
  acclient_2013_pseudo_c.txt:124137); thumb size = trackLen * ThumbRatio (min 8px); step ±1 line."
- git mv UiChatScrollbarTests.cs → UiScrollbarTests.cs; rename test class + replace
  every UiChatScrollbar reference with UiScrollbar (bodies unchanged).
- DatWidgetFactory: register Type 11 → new UiScrollbar() before the _ fallback case.
- ChatWindowController: change Scrollbar property type to UiScrollbar; replace the old
  "construct-remove-add" block with a "find factory-built UiScrollbar and bind in place"
  block (no RemoveChild/AddChild); keep `var track` assignment in scope so the Max/Min
  block's track.Left/track.Width reads still compile against UiElement?.
- AP-41 divergence register: update file:line to UiScrollbar.cs:35; narrow wording to
  "fallback only — single-tile drawn only when cap ids are unset; the chat controller
  passes all three cap ids so the 3-slice path is the active code path."
- Update inline UiChatScrollbar doc-comment references in UiScrollable.cs + UiChatView.cs.
- Full suite: 399 passed, 2 skipped (dat/tower fixture skips), 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:02:49 +02:00
Erik
34e79096f3 docs(D.2b): widget-generalization implementation plan
8-task TDD plan: chat golden fixture + resolved-Type conformance (Task 1,
empirically resolves the input's Type), then one-widget-per-commit migration —
UiScrollbar(11), UiButton(1), UiMenu(6), UiText(12)+the Type-12 flip,
UiField(3) — then thin the controller (Task 7, visual gate) and the gated
vitals UiText rewire (Task 8). Each task: failing test, register in the
factory switch, controller find-by-id binding, build+test green, commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:47:32 +02:00
Erik
56f5bc7aa1 docs(D.2b): add strategic-purpose section to widget-generalization design
Capture the 'why beyond chat' the user articulated: chat is the proving ground;
the real payoff is inventory/spell-bar/vendor/character-sheet/trade becoming
data-driven assembly + thin controller. Notes what carries forward (the generic
widget toolkit + the find-by-id controller pattern) vs what those windows still
need (ListBox/Panel + Field drag-drop, the window-manager half of Plan 2, and
per-domain item/container data).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:33:14 +02:00
Erik
b7f7e2b4ef docs(D.2b): widget-generalization design (Plan 2 widget piece)
Design for refactoring the hand-named chat widgets + Send/MaxMin click-wiring
into generic, Type-registered widgets built by DatWidgetFactory, collapsing
ChatWindowController (and, gated-last, VitalsController) to a thin retail
gm*UI::PostInit-style find-by-id binder.

Key finding that reframes the pass: the importer's base-chain Type resolution
is already retail-faithful, and Type 12 is UIElement_Text (a real behavioral
class), not a style prototype to skip — verified against
acclient_2013_pseudo_c.txt:115655. The generalization is therefore a
registration task (register Types 1/3/6/11/12 -> generic widgets, delete the
Type-12 skip), not a new mechanism.

Approved scope: full registry (bounded to the Types chat+vitals use; rest stays
UiDatElement fallback), chat-first, vitals rewire as the final separately-gated
step. 7-step one-widget-per-commit migration; new chat_21000006.json golden
fixture; vitals fixture stays frozen through steps 1-6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:26:32 +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
3d25e8760f @
docs(D.2b): chat-window re-drive implementation plan (8 tasks A-H)

TDD task breakdown for the data-driven chat window: ChatCommandRouter extraction
(A), UiChatView dat-font (B), UiScrollable + wire-in (C/C2), UiChatScrollbar (D),
UiChatInput (E), UiChannelMenu (F), ChatWindowController bind/route (G), GameWindow
cutover + divergence rows (H). Each ported widget cites its retail class::method.

Plan: docs/superpowers/plans/2026-06-15-chat-window-redrive.md
Spec: docs/superpowers/specs/2026-06-15-chat-window-redrive-design.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-15 22:04:35 +02:00
Erik
26cb34f126 @
docs(D.2b): chat-window re-drive design spec + list-ui-layouts research tool

Plan-2 chat piece of the LayoutDesc importer. Identifies the chat window as
LayoutDesc 0x21000006 (gmMainChatUI, element class 0x10000041) and grounds a
faithful, data-driven re-drive in the named retail decomp (ChatInterface +
gmMainChatUI + UIElement_Text/_Scrollable/_Scrollbar/_Menu) plus a user-provided
retail screenshot.

Design (full-faithful scope, user-approved):
- transcript = UIElement_Text 0x10000011 (dat font, bottom-pinned, 10k behead cap,
  pixel scroll, 1 line/wheel-notch)
- scrollbar = right-side track 0x10000012 + thumb 0x1000048c + up/down
- input = editable UIElement_Text 0x10000016 (caret, 100-entry history, Enter/Send)
- channel menu = UIElement_Menu 0x10000014 ("Chat" selector -> active channel)
- shared ChatCommandRouter extracted from ChatPanel
- screenshot correction: the four 0x10000522-525 left-edge elements are the
  numbered CHAT TABS (1-4), not scroll buttons (a research-agent inference the
  retail screenshot refutes)
- deferred (need non-UI plumbing, each gets a divergence row): tab switching/
  filtering, squelch, clickable name-tags, in-element word-wrap, styled runs,
  font config, opacity transition

Tooling: AcDream.Cli `list-ui-layouts <datdir> [0xRootType]` — read-only index of
every UI LayoutDesc by root element class + size + element-Type histogram; how the
chat layout was located (root type 0x10000041). Reusable for future panel re-drives.

Spec: docs/superpowers/specs/2026-06-15-chat-window-redrive-design.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-15 19:38:27 +02:00
Erik
50758d4795 docs(D.2b): chat-window re-drive session handoff (Plan 2 chat piece)
Captures: current hand-authored chat window (UiNineSlicePanel + UiChatView,
read-only, debug font), the importer toolkit to reuse, the retail gmMainChatUI
oracles, the open design questions (scope / behavioral widgets / dat font), and
the first research step (find the chat LayoutDesc id). Resume via brainstorming.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:07:05 +02:00
Erik
0474feb6ca docs(D.2b): correct roadmap/plan — vitals window IS resizable (resize shipped 8aa643f)
The earlier 'not resizable / fixed-size' note was wrong (inverted edge-flag
reading). Resize shipped: dat edge-anchors reflow per UIElement::UpdateForParentSizeChange.
Noted the two number-render fixes (submission-order + glyph pixel-snap).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:35:29 +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
Erik
825536a2bd docs(D.2b): re-retire TS-30 in register (restore branch state lost in --theirs merge)
The earlier 'git checkout --theirs' resolution of the register merge conflict
took main's whole file, which reverted two branch-only changes: IA-15 (re-added
in c100484) and the TS-30 retirement. TS-30 (flat-rect UI panels) was retired by
D.2b Spec 1 when UiNineSlicePanel shipped the 8-piece chrome and is doubly moot
now that vitals draw the dat chrome via the importer. Removed the TS-30 row +
its phase-gated reference; TS count 30->29. All section counts now match actual
rows (IA 15 / AD 27 / AP 37 / TS 29 / UN 5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:41:41 +02:00
Erik
c1004847a2 docs(D.2b): record vitals default-flip shipped (importer is now the default vitals)
Roadmap: update D.2b LayoutDesc importer entry to record that the default
flip shipped 2026-06-15 (bf77a23) — importer is the default at
ACDREAM_RETAIL_UI=1; vitals.xml + ACDREAM_RETAIL_UI_IMPORTER flag retired;
window movable, resize deferred to Plan 2 (WindowManager).

Plan: update "After Plan 1" to mark the flip DONE, clean up the Plan 2
description now that vitals.xml is gone.

Register:
- AP-37 "Why" cell: replace "Gated opt-in (ACDREAM_RETAIL_UI_IMPORTER)"
  with "Now the default vitals path (the hand-authored markup vitals was
  retired)" — the flag is gone.
- IA-15: add row (was missing from this branch) — D.2b retail UI design
  stance, updated to note that the vitals window is now rendered by the
  LayoutDesc importer (dat chrome elements), not UiNineSlicePanel;
  UiNineSlicePanel/RetailChromeSprites now back only chat window + plugin
  panels. IA count header 14 → 15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:36:47 +02:00
Erik
5ac9d8c19c merge: bring main into claude/hopeful-maxwell-214a12 (LayoutDesc importer branch)
main was 65 commits ahead of this branch's fork point. Only conflict was the
divergence register: both sides appended an 'AP-32' row. Resolved by keeping
main's AP-32..AP-36 (cell-shell lift, look-in cells, alpha deferral, dungeon
streaming, point lights) and renumbering the importer's row to AP-37; AP header
count -> 37. GameWindow.cs auto-merged cleanly. Verified: AcDream.App builds
0/0; AcDream.App.Tests 354 passed / 1 skipped / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:19:15 +02:00
Erik
07cf120939 docs(D.2b): mark LayoutDesc importer Plan 1 shipped; defer default-flip to Plan 2 (drag/resize)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 15:03:31 +02:00
Erik
4dcc90cb51 docs(D.2b): register AP-32 + IA-15 amend for importer; doc/test review fixes (N1/N4)
Process/quality items from the LayoutDesc-importer final review — no runtime
behavior change.

I1a — amend IA-15: the 8-piece chrome edge/corner→position mapping is no longer
a guess.  The LayoutImporter (ACDREAM_RETAIL_UI_IMPORTER) reads real LayoutDesc
dat data and resolves positions + sprite ids directly; locked by the conformance
fixture vitals_2100006C.json.  Residual risk trimmed to anchor resolution at
non-800×600 + controls.ini cascade.  Pointers added to LayoutImporter.cs and the
format-doc.

I1b — add AP-32: the importer collapses the dat's nested meter structure
(Type-7 → two Type-3 containers → three image-slice grandchildren each) into
UiMeter's programmatic 3-slice fields instead of building those nodes generically
and porting UIElement_Meter::DrawChildren.  Standalone Type-0 text elements are
also skipped (Plan 2).  Retail oracles: UIElement_Meter::DrawChildren @0x46fbd0,
UIElement_Text::DrawSelf @0x467aa0.

I1c — AP section header 31 → 32.

N1 — ElementReader.cs: comment at the Type-merge line explaining that a derived
Type 0 (text element) inherits the base's Type 12 (style prototype), which
DatWidgetFactory skips; safe for Plan 1 because vitals numbers render via
UiMeter.Label.  Format-doc §10: correct the "render as UiDatElement" sentence to
"skipped entirely" (Type-0 → inherits Type-12 via Merge → factory returns null).

N4 — new conformance test VitalsTree_TextLabel_InheritsFontDidFromBaseLayout:
walks the raw ElementInfo tree from the fixture and asserts at least one element
carries FontDid==0x40000000, proving Resolve()'s inheritance merge fired against
real dat data.  FixtureLoader gains LoadVitalsInfos() that returns the raw tree
without calling Build.

Tests: 36 pass (was 35), 0 errors, 0 warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 14:55:01 +02:00
Erik
67819f35a4 docs(D.2b): LayoutDesc format enumeration (importer groundwork)
Resolves all 6 open unknowns for Tasks 2–6 of the LayoutDesc importer plan:

1. Edge-anchor flags: 1=near-pin, 2=far-pin, 3=float-center, 4=stretch.
   The plan's assumption of 4="pinned to that side" is corrected — 1 is
   the near-pin, 4 is stretch (both sides). Revised ToAnchors signature given.

2. ElementDesc members: all are public FIELDS (not properties). X/Y/Width/
   Height/LeftEdge/etc. are uint. Type is uint (not enum). States is
   Dictionary<UIStateId, StateDesc>. Children is Dictionary<uint, ElementDesc>.

3. StateDesc shape: Properties is Dictionary<uint, BaseProperty> with concrete
   subclasses (ArrayBaseProperty, DataIdBaseProperty, IntegerBaseProperty, etc.).
   Font DID (0x1A) is ArrayBaseProperty[ DataIdBaseProperty{Value=0x40000000} ].
   Font color (0x1B) is ArrayBaseProperty[ ColorBaseProperty ]. Fill (0x69) is
   NOT in the dat — pushed at runtime by gmVitalsUI::Update.

4. DrawModeType enum: Undefined=0, Normal=1, Overlay=2, Alphablend=3.
   No "Stretch" value exists. Vitals uses Normal(1) and Alphablend(3) only.

5. Type values confirmed from RegisterElementClass: 3=Field/container,
   7=Meter→UiMeter, 9=Resizebar, 0xC=Text, 2=Dragbar, 12=style prototype (skip).

6. Inheritance chain: vitals text labels (Type=0) inherit from base element
   0x10000376 in layout 0x2100003F (Type=12), which carries font DID 0x40000000.
   The full per-vital sprite id tables for 0x2100006C are confirmed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 13:05:53 +02:00
Erik
a7875cde22 docs(D.2b): LayoutDesc importer implementation plan (Plan 1 — vitals conformance) 2026-06-15 12:46:55 +02:00
Erik
64146bfc2a docs(D.2b): LayoutDesc importer design spec (data-driven retail windows) 2026-06-15 12:38:34 +02:00
Erik
d2b8a51426 docs: wrap-up — file #137 (dungeon collision) + #138 (teleport-out world loading); close #135/#136
- #137: dungeon collision wrong at doors / wall openings (EnvCell collision; needs repro).
- #138: teleport OUT of a dungeon loads the outdoor world incompletely (missing trees/
  scenery, broken collision) + a position desync (avatar moves but player position doesn't)
  — hypothesised as the dungeon-streaming collapse→EXPAND gap (same machinery as #135).
- #135 marked DONE (user-verified FPS-steady dungeon login); #136 closed (editor-marker hide).
- CLAUDE.md current-state refreshed: #135/#136 shipped, A7 lighting + #137/#138 remaining.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:08:40 +02:00
Erik
fd0ecfcf2e docs: close #136 — red cone was an editor-only placement marker (fixed 6f81e2c)
Rewrite the #136 entry with the definitive root cause (editor-only dat placement
marker hidden by retail's distance degrade, inherited as visible from the WB-derived
render path) replacing the earlier refuted texture-pipeline hypothesis; mark FIXED.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 19:05:03 +02:00
Erik
b4ed8e7908 docs: file #136 — red-cone dungeon decoration renders red (frozen-phase render divergence)
Investigated the user-reported divergence (a solid-red cone in the 0x0007 dungeon
that retail doesn't draw). Narrowed by elimination:
- geometry, not VFX (survives particles-off)
- object 0x70007055 / Setup 0x020019F0, physState=0x1C — NOT NoDraw/Hidden
- its distinguishing texture 0x06006D65 (DXT1 256x128) DECODES tan/opaque offline,
  identical to a neighbour decoration (0x020019EE / tex 0x06006D63) that renders fine
- not a per-instance tint (hook dropped)
=> the red is introduced at runtime in the WB bindless texture-array upload/sampling
path (a #105-class "samples undefined until flushed" / layer-handle misassignment),
possibly lighting. Both WB-render-migration and sky/lighting are FROZEN phases, so the
fix awaits explicit sign-off. Full diagnosis + reusable diagnostic approach in the issue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 18:11:15 +02:00
Erik
2f4520ee12 docs(D.2b): mark D.2b + D.4 shipped (Spec 1 — markup engine + retail vitals)
Roadmap: D.2b (custom retail-look backend) and D.4 (dat sprites + 9-slice +
DrawSprite) both shipped this session via the Spec-1 work — the UiHost-based
markup engine (MarkupDocument + ControlsIni + IUiRegistry) rendering a
markup-driven retail Vitals panel (8-piece dat chrome + red/gold/blue bars).
Records the direct-RenderSurface decode finding + the confirmed chrome sprite
ids. Remaining D.2b polish (gradient bar sprite, AcFont/D.3, input integration,
LayoutDesc importer, D.5 panels) noted inline.

Full suite green (2413 passed / 0 failed / 3 pre-existing skips).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:50:42 +02:00
Erik
2c923755c4 fix(G.3): place the player on the cell floor for an indoor dungeon login (#135 follow-up)
Two regressions from the pre-collapse (712f17f), found by live gate + a runtime
probe:

1) Login-into-dungeon stopped loading the dungeon. The login-hold streaming
   observer fell through to the OFFLINE fly-camera branch once
   _lastLivePlayerLandblockId was filtered to the player guid (a dungeon-local
   NPC used to keep it pinned). A camera-derived observer far from the
   pre-collapsed dungeon tripped ExitDungeonExpand and unloaded it. Fix: a LIVE
   in-world session never uses the fly camera for the observer — it follows the
   player's server landblock, falling back to the recentered spawn center
   (_liveCenterX/Y). The fly camera is the OFFLINE observer only.

2) Even with the dungeon resident, auto-entry hung: the #106 "ground ready" gate
   required SampleTerrainZ under the spawn, but a dungeon's negative-offset cells
   place the spawn's WORLD position in a NEIGHBOUR terrain landblock the #135
   collapse deliberately doesn't load (probe: cellReady=True, terrReady=False
   forever). The terrain gate is wrong for an indoor spawn — the player lands on
   the EnvCell FLOOR. Fix: gate an indoor (hydratable) spawn/teleport on
   IsSpawnCellReady, not the terrain heightmap; outdoor (and unhydratable→demote)
   spawns still hold on terrain. Applied to both isSpawnGroundReady (login auto-
   entry) and TeleportArrivalReadiness (teleport). This is the faithful equivalent
   of retail's synchronous cell load + place-on-floor; the pre-#135 terrain hold
   only passed because the 25x25 window streamed the neighbour terrain.

Verified live: login into 0x0007 → auto-entered player mode, snapped to
0x00070145, dungeon renders, FPS steady. Register AD-2 amended.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:13:12 +02:00
Erik
b18403da02 feat(D.2b): wire UiHost + live retail Vitals panel (render-only); retire TS-30
Wires the dormant AcDream.App/UI retained-mode tree into GameWindow under
ACDREAM_RETAIL_UI=1: an 8-piece dat-sprite UiNineSlicePanel framing three
UiMeter vital bars bound to the existing VitalsVM. Render-only (UiHost input not
yet bridged to the InputDispatcher — next sub-phase). Coexists with the ImGui
devtools path; no regression there.

Visually verified against a live retail client: the bars match retail's vitals
structure (three stacked horizontal bars, current/max numbers centered) — so the
earlier "orbs" assumption was wrong (retail vitals ARE bars), and stamina is GOLD
not cyan (the #10F0F0 research note was wrong). UiMeter gains a centered numeric
Label (stub debug font for now). Spec §8 + the markup example corrected to match.

Bookkeeping: retired divergence row TS-30 (flat-rect panels -> real dat chrome)
and added IA-15 (our UiHost/markup engine vs keystone.dll's LayoutDesc tree).

Remaining polish (filed, §15): glassy gradient bar fill sprite + the retail dat
font for the numbers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:56:57 +02:00
Erik
712f17f0f2 fix(G.3): pre-collapse dungeon streaming at login/teleport — kill the login FPS ramp (#135)
On login (or teleport) into a dungeon, FPS started ~10 and climbed over ~30 s.
Root cause: the dungeon "collapse" (which shrinks the 25x25 streaming window to
the player's single dungeon landblock — AC dungeons have no neighbours) only
fires once the per-frame `insideDungeon` gate reads true, and that gate keys on
the physics CurrCell, which isn't set until the player is PLACED, which waits for
the dungeon landblock to hydrate. So during the whole hydration window NormalTick
bootstraps the full window — ~24 unrelated ocean-grid neighbour dungeons + their
~19k entities each — and the collapse only mops them up afterward. That mop-up is
the ramp.

Fix: trigger the SAME collapse early, the instant we recenter the streaming center
onto a sealed dungeon cell, before the first NormalTick.

- StreamingController.PreCollapseToDungeon(cx,cy): fires EnterDungeonCollapse
  early (idempotent). The expensive neighbour window is never enqueued.
- GameWindow.IsSealedDungeonCell(cellId): reads the EnvCell dat SeenOutside flag
  (CurrCell is null pre-placement) — the same flag ObjCell.SeenOutside and the
  per-frame gate use, so the early decision matches the eventual one. Distinguishes
  a real dungeon from a cottage/inn interior (SeenOutside → keeps its outdoor
  surround). Excludes the 0xFFFE/0xFFFF structural shell ids so an outdoor spawn id
  can't type-confuse a LandBlock record as an EnvCell.
- Hooks: OnLiveEntitySpawnedLocked (login) + OnLivePositionUpdated (teleport).
- Observer robustness: during a teleport PortalSpace hold the streaming observer
  follows the recentered destination, not the frozen pre-teleport position (which
  could drift >=2 landblocks off and trip ExitDungeonExpand). And
  _lastLivePlayerLandblockId is now filtered to the player guid (resolves the
  Phase A.1 TODO) so a stray NPC UpdatePosition can't drift the login-hold observer
  off the dungeon.

Faithful EARLY trigger of the existing AP-36 collapse mechanism, not a new
workaround — AP-36 amended in the same commit. Adversarially reviewed across
timing / threading / faithfulness lenses; 5 new tests including the real runtime
ordering (Tick bootstraps, then PreCollapse cancels). Core suite green (1463).

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