Commit graph

14 commits

Author SHA1 Message Date
Erik
defbde1f86 feat(studio): Attributes tab Pass 2 — click-to-select (highlight + footer B + raise triangles) + tab states
- UiClickablePanel: new UiPanel subclass with OnClick action + HandlesClick=true
  so row clicks survive whole-window-Draggable ancestor frames.

- CharacterStatController overhaul (Pass 2):
  - Tab bar button states: SetButtonStateRecursive walks each tab group container
    (0x10000228/229/538) setting UiButton.ActiveState="Open" (Attributes) or
    "Closed" (Skills/Titles) via UIStateId enum string names from DatReaderWriter.
  - Row click: 9 rows become UiClickablePanel; sel[] mutable box drives footer/highlight.
  - Toggle: clicking the same row deselects (→ footer State A); click a new row
    updates title="Attrib: value", line-1 label="Experience To Raise:", line-1
    value=cost, line-2="Unassigned Experience:" in both states.
  - Row highlight: BackgroundColor=HighlightBg (semi-translucent gold) on selected row.
  - Raise buttons (0x10000246 ×1 + 0x100005EB ×10): hidden initially; shown on
    selection with ActiveState="Normal" (affordable) or "Ghosted" (cost=0 or unaffordable).
    CollectButtonsById tree-walk finds ALL copies of the button across tab-page mounts
    (not just the last-registered _byId copy) so all instances are controlled.

- CharacterSheet: AttributeRaiseCosts long[] (Strength…Mana raise costs in retail
  display order; cost=0 → max/disabled row demos the Ghosted button state).
- SampleData.SampleCharacter: fills AttributeRaiseCosts[9] — Strength/Quickness=0
  (maxed), Focus@10→110 matching the retail screenshot (spec §4).

- 35 new tests (total 673 pass) covering: row click→footer B title/line1/line2,
  toggle deselect→footer A, switch row, highlight set/clear, raise button
  hidden/Normal/Ghosted/deselect, tab Open/Closed states, GetRaiseCost helper,
  GetRowName helper, SampleData fixture sanity.

Console.WriteLine("[CharacterStat] Row click: index=N → selected=N (Name)") fires
on every click for the user's live verification in the studio.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 21:48:16 +02:00
Erik
21d8485053 feat(studio): forward canvas mouse to the previewed panel (interactive preview)
Canvas clicks now reach the panel UiHost so buttons, tabs, and slots
respond to user interaction. Previously only UiRoot.Pick (inspector
selection) received click events; the panel itself was inert.

Key changes:
- StudioInspector.DrawCanvas now returns a CanvasInputEvent struct
  (was nullable click tuple) — carries isHovered, move position,
  leftDown/Up, and scroll delta, all in panel-local pixels.
- Coordinate mapping: panel_local = mouse_screen - GetItemRectMin()
  (1:1, no scale factor). V-flip (uv0.Y=1, uv1.Y=0) makes screen
  top = panel Y=0, so NO extra Y inversion. Documented in comments.
- StudioWindow.OnLoad: removed WireMouse — raw Silk window coords are
  offset by the canvas sub-window position and land in the wrong place.
  WireKeyboard kept (keyboard input needs no spatial remapping).
- StudioWindow.OnRender: forwards OnMouseMove always (hover states),
  plus OnMouseDown/Up/OnScroll in Interact mode. Console.WriteLine
  on each forwarded left-click for live verification.
- Interact/Inspect toggle: checkbox in the Studio toolbar (default
  Interact). Inspect mode restores old click-to-select-element behavior
  while still forwarding OnMouseMove for hover states.
- CanvasCoordMappingTests: 7 pure-math unit tests covering the
  origin, interior points, corner, chrome OOB, and the no-extra-flip
  invariant (no GL required).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 21:32:00 +02:00
Erik
eccacc59de fix(D.2b): Attributes tab — fill panel height, center row icons, footer at bottom
Root cause: the sub-layout 0x2100002C (design H=337px) mounted into tab slot
0x1000022B (H=575px) via ShouldMountBaseChildren. ElementReader.Merge takes the
derived (sub-layout) H=337 as canonical, so the background element's first
ApplyAnchor call captured _amB=238 and the list box captured _amB=65 — both
stayed at their 337px-parent sizes even though the slot is 575px.

Fix: CascadeHeight in LayoutImporter.Resolve mirrors retail's
UIElement::UpdateForParentSizeChange. When ShouldMountBaseChildren fires and the
slot is taller than the base design, every full-stretch background child has its
height cascaded: Top+Bottom anchors → stretch, Bottom-only → pin-to-bottom,
Top-only/None → unchanged. This propagates the correct bottom margins through the
entire subtree before the first render frame.

Layout result (confirmed via headless screenshot):
- List box grows from 160px → 398px (9 rows × 44px ≈ 396px)
- Footer elements move from abs Y≈307 → abs Y≈545 (matching retail dump)
- Separator moves to abs Y≈535

Row constants updated: RowHeight 44px, IconSize 24px, RowPadX 4px, IconGap 6px.
Footer State-A text corrected per spec: title="Select an Attribute to Improve",
line-1 label="Skill Credits Available:", line-1 value=SkillCredits (96),
line-2 label="Unassigned Experience:", line-2 value=UnassignedXp (formatted N0).
CharacterSheet.UnassignedXp (long, retail InqInt64(2)) added.
SampleData.SampleCharacter().UnassignedXp = 87_757_321_741L.
5 footer tests renamed + assertions updated; SampleCharacter_UnassignedXp_IsSet added.
639 tests green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 21:19:45 +02:00
Erik
902160098a feat(D.2b): Character Attributes tab — 9-row list (icons + values + vitals) + footer State-A
Pass 1 of the Attributes tab interactive controller. Replaces the placeholder
UiText attribute list with 9 real manual-layout rows matching retail's
gmAttributeUI::PostInit (0x0049db70) structure:
- 6 attribute rows (Strength/Endurance/Coordination/Quickness/Focus/Self) in
  retail display order (Coord enum 4 before Quick enum 3 — spec §1)
- 3 vital rows (Health/Stamina/Mana) with cur/max format per
  Attribute2ndInfoRegion::Update (0x004f19e0)
- Each row: icon (UiText.BackgroundSprite = 0x06xxxxxx RenderSurface via spriteResolve),
  name (left-justified), value (new RightAligned mode)

Icon DataIDs from SubMap 0x25000006/0x25000007 via spec §2:
  STR 0x060002C8, END 0x060002C4, COORD 0x060002C9, QUICK 0x060002C6,
  FOCUS 0x060002C5, SELF 0x060002C7, HP 0x06004C3B, SP 0x06004C3C, MP 0x06004C3D

Footer State-A (DisplayDefaultFooter 0x0049cde0):
  0x1000024e title = "", 0x10000243 line-1-value = "Select an Attribute to Improve",
  0x10000245 line-2-value = SkillCredits (InqInt(0x18))

Other changes:
- UiText: add RightAligned bool (single-line right-justified, mirrors Centered path)
- CharacterSheet: add SkillCredits property (retail InqInt(0x18))
- SampleData: update to spec fixture values (Str/Quick=200, rest=10, SkillCredits=96,
  Health=5/5, Stamina=10/10, Mana=10/10)
- FixtureProvider: pass stack.ResolveChrome as spriteResolve for icon rendering
- Tests: 13 targeted tests (9-row count, row order, attr+vital values,
  icon DataIDs, RightAligned flag, footer State-A all 5 elements)

Pass 2 (selection/raise buttons) is separate per spec.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 20:36:00 +02:00
Erik
0e644b5887 fix(studio): Character window — bind the REAL Attributes-tab elements (no guessing)
Redo of the character pilot per faithful-port rules. The previous pilot GUESSED a text
report and put it on the wrong window. The decomp (verified by dumping acdream's
importer-resolved tree) shows LayoutDesc 0x2100002E is the tabbed Attributes/Skills/Titles
window: its tab-content slot 0x1000022B mounts sub-layout 0x2100002C (gmAttributeUI) which
chains into the gmStatManagementUI header. The importer ALREADY mounts every content element
(FindElement resolves name 0x10000231, heritage 0x10000232, PK 0x10000233, level 0x1000023B,
total-XP 0x10000235, XP meter 0x10000236 → UiMeter, list box 0x1000023D) — EventId was a red
herring (the dat id lives in _byId, not the widget's EventId field).

CharacterStatController (port of gmStatManagementUI::UpdateCharacterInfo 0x004f0770 +
UpdateExperience 0x004f0a70 + UpdatePKStatus 0x004f00a0) binds those real elements: name,
heritage, PK status, level, total XP, the XP-to-level meter fill, and the six innate
attributes in the list box. Studio (--layout 0x2100002E) now renders the actual panel
content, not an overlay. 16 controller tests green; full App suite stays green.

Refinements with known decomp sources (follow-ups): tab-button active/inactive state so the
3 tabs draw their sprites; exact label wording from the StringTable (table 0x10000001 →
dat 0x31000001); the full AttributeInfoRegion row template (column-aligned values + raise
buttons). CharacterController (text-report 0x2100001A) retained for that separate sub-panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:47:33 +02:00
Erik
4988dd4dfe feat(studio): Character panel pilot — gmCharacterInfoUI report text + menu-bar picker
Task A — CharacterController (LayoutDesc 0x2100002E)
- CharacterSheet.cs: data record for all fields used by the report sections,
  with retail property-id citations per field (DateOfBirth 0x62, TotalPlayTime
  0x7d, NumDeaths 0x2b, SkillCredits 0xb5/0xc0, AugmentationStat 0x162,
  EncumbranceVal 0x5/0xe6).
- CharacterController.cs: static Bind(layout, data, datFont) wires element
  0x1000011d (m_pMainText, confirmed gmCharacterInfoUI::PostInit 0x004b86f0) as
  a UiText; LinesProvider builds the report. Report section order mirrors
  gmCharacterInfoUI::Update (0x004ba790):
  1. UpdatePlayerBirthAgeDeaths 0x004b8cb0 — birth/age/deaths
  2. UpdateEnduranceInfo 0x004b8eb0 — vitals (H/S/M cur/max)
  3. UpdateInnateAttributeInfo 0x004b87e0 — 6 attrs InqAttribute 1,2,4,3,5,6
     = Strength, Endurance, Quickness, Coordination, Focus, Self
  4. UpdateFakeSkills 0x004b8930 — skill credits (InqInt 0xb5 / 0xc0)
  5. UpdateAugmentations 0x004b9000 — aug name (InqInt 0x162 switch 1..0xb)
  6. UpdateLoad 0x004b8a20 — burden cur/max/pct
  Element 0x1000011d imports as UiText (Type 12); multi-line text set via
  LinesProvider — same pattern as ChatWindowController transcript.
- SampleData.SampleCharacter(): plausible retail-scale CharacterSheet fixture.
- FixtureProvider.Populate case 0x2100002Eu: CharacterController.Bind with
  SampleData.SampleCharacter + VitalsDatFont.
- CharacterControllerTests.cs: 10 pure data-wiring + content tests (no GL/dats).

Task B — Studio panel picker → main menu bar
- StudioWindow.OnRender: replaced the floating "Studio" toolbar window (kToolbarH
  40px) with ImGui.BeginMainMenuBar / EndMainMenuBar (kMenuBarH 22px). Panel
  picker combo now lives in the always-on-top menu bar and cannot be occluded by
  Tree/Canvas/Props panes. paneY reduced from 40→22, freeing ~18px for the canvas.

Build: dotnet build green (0 CS errors; DLL-lock warnings are AcDream.App being
live — not compile errors). Full suite: 619 passed, 2 skipped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 18:37:30 +02:00
Erik
c03a241884 feat(studio): UX pass — selection highlight, in-studio panel picker, fixed layout
Fix 1 (Selection highlight): StudioInspector.DrawCanvas draws a bright-green
2px outline over Selected using the window draw list. ScreenPosition maps
directly to FBO pixel space; rectMin offsets into screen space.

Fix 2 (In-studio panel picker): DrawToolbar (BeginCombo over ListSlugs output).
StudioWindow resolves _dumpFile once in OnLoad, tracks _currentSlug, and calls
LoadDumpPanel on combo change. LoadDumpPanel removes old root via RemoveChild,
loads new slug, resets Selected. No relaunch needed.

Fix 3 (Fixed layout): All four panes use SetNextWindowPos + SetNextWindowSize
with ImGuiCond.FirstUseEver. Toolbar (0,0 x windowW x 40), tree (0,40 x 280),
canvas (280,40 x centre), props (right-340,40 x 340). User can drag from these.

Build + dotnet test green (609 passed, 0 failed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:58:50 +02:00
Erik
f778b100b7 feat(studio): headless --screenshot mode (render panel to PNG + exit)
Add `--screenshot <path>` to the UI Studio: when set the window is
created hidden (WindowOptions.IsVisible = false), the panel is loaded
and rendered into PanelFbo on the first OnRender tick, pixels are read
back with the new PanelFbo.ReadColorRgba(), rows are flipped (GL
bottom-left → PNG top-left), and the result is saved via ImageSharp
SaveAsPng before calling _window.Close().

Render size is derived from the loaded root's Width/Height (clamped
256–2048 px); falls back to 1280×720 when the root has no explicit
size.  In headless mode ImGui, the inspector, and input wiring are
all skipped — only PanelFbo is created.  Interactive path is
unchanged.  SixLabors.ImageSharp 3.1.12 was already a direct dep of
AcDream.App (Phase O-T7); no new PackageReference needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 16:36:10 +02:00
Erik
e3de7f0dab feat(studio): dump LayoutSource — preview any retail window from the UI dump
Adds Task 4b: a second load path for the UI Studio that reads the committed
retail UI layout dump (docs/research/2026-06-25-retail-ui-layout-dump.json)
and renders any of the 26 retail windows as a static sprite hierarchy.

New files:
- src/AcDream.App/Studio/UiDumpModel.cs — POCOs + System.Text.Json parse of
  the dump (UiDump, DumpPanel, DumpNode, DumpRect, DumpStateSet, DumpImage,
  DumpState + UiDumpModel static helpers: Parse, ListSlugs, PickImageId).
- src/AcDream.App/Studio/DumpLayout.cs — DumpLayout.Load(path, slug, resolve,
  out err): parses the dump, finds the panel by slug, builds a UiElement tree.
  Internal DumpSpriteElement draws its sprite via DrawSprite (not reusing
  UiDatElement — avoids the ElementInfo/StateMedia dat-import dependency for
  this static mockup). DumpGroupElement is a transparent container for Group
  nodes. Rect basis is ABSOLUTE in the dump (verified: inventory root at
  absolute x=500 and its children also start at x≈500 — child offset from
  parent is 0–50px, not 500px); DumpLayout subtracts parent rect to produce
  parent-relative Left/Top for each child.
- tests/AcDream.App.Tests/Studio/DumpLayoutTests.cs — 5 tests covering:
  inventory load with 0x100001D5 check + >= 40 nodes, unknown slug → null+err,
  root at origin, children are parent-relative, all 26 slugs smoke-load.

Modified files:
- StudioOptions: adds DumpSlug + DumpFile fields; --dump <slug> and
  --dump-file <path> args; ResolveDumpFile() walks up to the solution root
  to find the default dump JSON (mirrors ConformanceDats.SolutionRoot()).
  --dump suppresses the default vitals layout so the two modes are exclusive.
- StudioWindow.OnLoad: when DumpSlug is set, loads via DumpLayout (no
  FixtureProvider, no controllers — static structure only); else falls
  through to the existing LayoutSource + FixtureProvider path.

Results: DumpLayoutTests 5/5 passed; full AcDream.App.Tests 609 passed, 2
skipped (same as before); dotnet build green, 0 warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 16:25:23 +02:00
Erik
9ed9d8dbd9 fix(studio): Task 4 — bind PaperdollController + wire empty-slot sprites
FixtureProvider.Populate for 0x21000023 was binding only
InventoryController and omitting PaperdollController, so the
~21 equip slots rendered empty even though sample equipped items
existed in the table.  Also, the three per-list empty-slot
sprites were not being resolved from the dat (contentsEmpty /
sideBagEmpty / mainPackEmpty all defaulted to 0), so the slot cells
had no background art.

Fixes:
- FixtureProvider.Populate gains a DatCollection dats param
  (mirrors the GameWindow.OnLoad lookup at line 2233-2235).
  The 0x21000023 case now resolves all three empty sprites via
  ItemListCellTemplate.ResolveEmptySprite and passes them to
  InventoryController.Bind.
- PaperdollController.Bind is now called after InventoryController
  in the same 0x21000023 case, matching GameWindow:2257-2265
  (same layout subtree, contentsEmpty as the equip-slot placeholder,
  sendWield: null since there is no live session in the studio).
- StudioWindow.OnLoad passes _dats! to the updated Populate signature.

SampleData.AddEquipped was already correct: MoveItem(guid, PlayerGuid,
-1, equipMask) at ClientObjectTable.cs:134 sets
CurrentlyEquippedLocation = newEquipLocation and calls Reindex which
places the item in _containerIndex[PlayerGuid].  No SampleData change
needed.

Tests: 2 new assertions in FixtureProviderTests —
  sideBags_matchInventoryControllerFilter (Type.HasFlag(Container)
  || ItemsCapacity > 0 filter must match both bags) and
  equippedItems_retainLocationAndAreInContents (equipped items have
  CurrentlyEquippedLocation != None AND appear in GetContents so the
  paperdoll controller can find them).  604 pass / 2 skip / 0 fail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 15:59:13 +02:00
Erik
0bdc866c15 feat(studio): FixtureProvider — sample data populates the 2-D panels
Task 4 of the UI Studio plan: adds SampleData (static ClientObjectTable
builder with a synthetic player, 6 loose items, 2 side bags, and 3 equipped
pieces) and FixtureProvider (switches on layoutId and calls the production
controller Bind methods — VitalsController, ToolbarController,
InventoryController — so the studio previews panels with plausible data
instead of empty widgets).

Icon ids approach: raw-resolve stub — resolves the base iconId via
RenderStack.ResolveChrome and returns the GL handle directly. This is v1
(single-layer icon); the full 5-layer IconComposer composite is a live-game
concern, not a layout-preview concern.

StudioWindow wires FixtureProvider.Populate after _source.Load; the
ClientObjectTable is stored on _objects so controller event subscriptions
(ObjectAdded/ObjectMoved) remain live for the window's lifetime.

4 new FixtureProviderTests (SampleTable_hasPackContents,
SampleTable_hasWeaponAndArmor, SampleTable_hasEquippedItems,
SampleTable_hasSideBags) — all pass. Full App suite: 602 passed / 2
skipped (pre-existing) / 0 failed. Build green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 15:45:11 +02:00
Erik
101a35cc2d fix(studio): Task 3 review — UiRoot.Pick, RenderStack IDisposable, dt cleanup
Code-review follow-ups to the ImGui inspector:
- Add public UiRoot.Pick(x,y) over the private HitTestTopDown (honors
  Z-order + modal exclusivity); StudioWindow uses it instead of a manual
  UiElement.HitTest with subtracted ScreenPosition.
- RenderStack : IDisposable — disposes the GL pieces it owns in one place;
  StudioWindow OnClosing + Dispose both call _stack?.Dispose(), closing the
  error-path leak (only UiHost was disposed on the Dispose-without-OnClosing
  path).
- Drop the stale _dt field; OnRender passes its own dt to Tick + BeginFrame.
- Fix a stale PanelFbo comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 15:14:47 +02:00
Erik
d2240974ec feat(studio): ImGui inspector — canvas FBO, element tree, props, click-to-inspect
Task 3 of the acdream UI Studio plan. The studio previously drew the
panel straight to the window; it now renders the panel into an off-screen
FBO (PanelFbo) and displays it in an ImGui Canvas pane alongside a Tree
pane (recursive element hierarchy, click-to-select) and a Properties pane
(EventId/type/rect/anchors/ZOrder/flags of the selected element).

Click-to-inspect: a left-click inside the Canvas calls UiElement.HitTest
on the panel root (same-assembly internal access) and selects the topmost
element at the cursor, wiring the canvas directly to the tree selection.

PanelFbo lifecycle mirrors PaperdollViewportRenderer (RGBA8 color +
Depth24Stencil8 renderbuffer, GLStateScope-sealed, lazy resize on size
change). V-flip in DrawCanvas (uv0=(0,1)/uv1=(1,0)) corrects GL's
bottom-left FBO origin to ImGui's top-left convention so click Y maps
directly to panel-local Y without inversion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 15:06:12 +02:00
Erik
df6b5b3b39 feat(studio): StudioWindow + LayoutSource — render a dat panel standalone
Task 2 of the acdream UI Studio plan. Adds three new files under
src/AcDream.App/Studio/ and one new test file:

- StudioOptions.cs: record + Parse() for the ui-studio CLI args (positional
  dat dir or ACDREAM_DAT_DIR, --layout 0xNNNN, --markup <path>; defaults to
  vitals 0x2100006C when neither layout nor markup given).
- LayoutSource.cs: wraps LayoutImporter.Import for dat-backed layouts; markup
  path sets LastError "markup unsupported (Task 6)" and returns null for now.
  Exposes Kind / LayoutId / MarkupPath / LastError / CurrentLayout / Reload().
- StudioWindow.cs: Silk.NET 1280×720 GL 4.3 window; boots RenderBootstrap,
  wires input to UiHost, loads the panel via LayoutSource, adds the root to
  UiHost.Root.AddChild. QualitySettings resolved the same way GameWindow.Run()
  does (SettingsStore → QualitySettings.From → WithEnvOverrides).
- Program.cs: ui-studio dispatch at the top of top-level statements (before
  the dat-dir parse) so `dotnet run -- ui-studio` routes to StudioWindow.
- LayoutSourceTests.cs: dat-gated test (skips when dats absent); verifies that
  the vitals LayoutDesc (0x2100006C) loads, root is non-null, byId contains the
  vitals root element (0x100005F9), and Kind == DatLayout. Passes (1/1 with dats
  present; silently skips on CI).

Note: the spec asserts FindElement(0x2100006Cu) — that id is the LayoutDesc
dat id, not a widget element id. The actual vitals root element id is 0x100005F9
(confirmed from the vitals_2100006C.json fixture). The test uses the correct
element id and documents the discrepancy.

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