Merge claude/hopeful-maxwell-214a12 — D.2b UI Studio + faithful importer + Character window
UI Studio (preview panels through the production renderer), importer dat-fidelity (Fix A/B/C/4/5: the importer carries dat font/justification/color; boundary look=importer / state=runtime), and the Character window Attributes tab (reads as retail). 3062 tests green. Handoff: docs/research/2026-06-26-mockup-stage-handoff.md. # Conflicts: # docs/ISSUES.md # docs/architecture/retail-divergence-register.md
This commit is contained in:
commit
ca94b479bf
61 changed files with 125881 additions and 44 deletions
251
docs/superpowers/specs/2026-06-23-d2b-paperdoll-slice2-design.md
Normal file
251
docs/superpowers/specs/2026-06-23-d2b-paperdoll-slice2-design.md
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
# D.2b Sub-phase C, Slice 2 — paperdoll 3-D doll + the "Slots" toggle (design)
|
||||
|
||||
**Date:** 2026-06-23
|
||||
**Branch:** `claude/hopeful-maxwell-214a12` (Slice 1 shipped; `main` ff-merged to tip).
|
||||
**Status:** APPROVED design (brainstorm complete). Next: writing-plans → subagent-driven implementation → visual gate.
|
||||
**Predecessor:** `docs/research/2026-06-23-paperdoll-slice2-handoff.md`; Slice-1 spec `2026-06-22-d2b-paperdoll-slice1-equip-slots-design.md`.
|
||||
**Read with:** the corrected model in the D.2b SSOT (`claude-memory/project_d2b_retail_ui.md`, the SLICE 1 entry) + `docs/research/2026-06-16-equipment-paperdoll-deep-dive.md` §5.
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal + corrected model (user axiom)
|
||||
|
||||
The paperdoll panel has **two mutually-exclusive views** toggled by the **"Slots" button**
|
||||
(`0x100005BE` = `m_SlotCheckbox`):
|
||||
|
||||
- **Doll-view (default, button OFF):** the **live 3-D character** (the "doll" — naked if nothing
|
||||
equipped) + the **12 non-armor** equip slots. The 9 armor slots are hidden.
|
||||
- **Slot-view (button ON):** the doll disappears; the **9 armor slots** become visible.
|
||||
|
||||
The "figure" **IS** the live 3-D character (a re-dressed clone of the local player), **not** per-slot
|
||||
silhouette sprites. Slice 1 already shipped the 21 equip slots + wield/unwield + visible empty-slot
|
||||
frames, showing all slots at once. **Slice 2 adds (A) the toggle and (B) the 3-D doll viewport on top,
|
||||
extending — never rewriting — `PaperdollController`.**
|
||||
|
||||
### Scope
|
||||
|
||||
**In:** the Slots toggle; the `UiViewport` (dat Type `0xD`) hosting a re-dressed player clone;
|
||||
render-to-texture compositing; live re-dress on equip/unequip (ObjDescEvent `0xF625`); fixed camera,
|
||||
one distant light, heading 191.37°, idle animation.
|
||||
|
||||
**Deferred to polish (NOT this slice):** per-race camera framing (`UpdateForRace`), part-selection
|
||||
lighting ("which piece is this?" highlight), click-to-rotate, the doll click-map (`m_paperDollDragMask`
|
||||
interactivity), Aetheria sigil slots.
|
||||
|
||||
---
|
||||
|
||||
## 2. Decomp anchors (named-retail `acclient_2013_pseudo_c.txt`) — do not re-derive
|
||||
|
||||
| What | Function / addr | Lines | Facts |
|
||||
|---|---|---|---|
|
||||
| **The toggle** | `gmPaperDollUI::ListenToElementMessage` `0x004a5c30` | 175674-175706 | `idMessage==1 && idElement==0x100005be`: read checkbox attr `0xe`. **Checked → slot-view:** `m_pPaperDoll.SetVisible(0)`, `m_paperDollDragMask.SetVisible(0)`, then `SetVisible(1)` on the 9 armor slots. **Unchecked → doll-view:** the inverse. |
|
||||
| **The 9 armor slots** | `gmPaperDollUI::PostInit` `0x004a5360` | 175412-175508 | The exact set `PostInit` calls `SetVisible(0)` on AND the toggle flips: head `0x100005ab`, chest(armor) `0x100005ac`, abdomen `0x100005ad`, upper-arm `0x100005ae`, lower-arm `0x100005af`, hand `0x100005b0`, upper-leg(armor) `0x100005b1`, lower-leg(armor) `0x100005b2`, foot `0x100005b3`. The other 12 slots are **never** hidden at init. |
|
||||
| **Viewport init** | `gmPaperDollUI::PostInit` | 175509-175535 | Find `0x100001d5` → cast Type `0xd`; `SetCamera(&dir,&pos)`; `SetLight(DISTANT_LIGHT, 2.0, &dir)`; `CreatureMode::UseSharpMode`; `RedressCreature`. |
|
||||
| **Camera immediates** | `PostInit` | 175521-175527 | cam pos `(0x3df5c28f, 0xc019999a, 0x3f6147ae)` = **(0.12, −2.4, 0.88)**; target/look `(0,0,0)`. (Arg order pos-vs-dir confirmed at impl via `UIElement_Viewport::SetCamera`.) |
|
||||
| **Light immediates** | `PostInit` | 175529-175533 | dir `(0x3e99999a, 0x3ff33333, 0x3f266666)` = **(0.3, 1.9, 0.65)**; intensity **2.0**; type `DISTANT_LIGHT`. (3rd float recovered from the `"ff&?"` strncpy artifact = `0x3f266666`.) |
|
||||
| **The checkbox default** | `PostInit` | 175585 | `SetAttribute_Bool(m_SlotCheckbox, 0xe, 0)` → start unchecked = doll-view. |
|
||||
| **Re-dress** | `gmPaperDollUI::RedressCreature` `0x004a3bc0` | 173997-174012 | Built **once** (lazy): `makeObject(player)` clone → `set_heading(191.367905°)` → `set_sequence_animation(m_didAnimation.id, 1,1,0)` → `CreatureMode::AddObject`. **Every call:** `DoObjDescChangesFromDefault(clone, get_player_visualdesc())` = re-dress with the same ObjDesc apply the in-world renderer uses. |
|
||||
| **Per-race camera (DEFERRED)** | `gmPaperDollUI::UpdateForRace` `0x004a3ed0` | 174138-174180 | Camera distance varies per body-type: case 6/7 y=`0xc0400000`=−3.0; case 8 y=`0xc059999a`=−3.4, z=`0x3f800000`=1.0; etc. Polish. |
|
||||
|
||||
---
|
||||
|
||||
## 3. acdream seams (verified against source this session)
|
||||
|
||||
- **Render order:** `GameWindow.OnRender` runs all 3-D world passes, then `_uiHost.Tick()`/`_uiHost.Draw()`
|
||||
at **GameWindow.cs:9009-9012**. The doll RTT pass slots in after the world passes, before the UI pass.
|
||||
- **Single-entity draw:** `WbDrawDispatcher.Draw(ICamera camera, IEnumerable<(LandblockId, AabbMin,
|
||||
AabbMax, IReadOnlyList<WorldEntity> Entities, IReadOnlyDictionary<uint,WorldEntity>? AnimatedById)>,
|
||||
…, HashSet<uint>? animatedEntityIds, …)` (WbDrawDispatcher.cs:881). Sets `uViewProjection` from
|
||||
`camera.View*camera.Projection` (`:894`) and `uLightingMode=0` (Lambert sun + per-entity point sets,
|
||||
`:898`). **A second pass with a doll camera + a one-entry landblock tuple is supported.**
|
||||
- **Camera:** `ICamera` is `{ Matrix4x4 View; Matrix4x4 Projection; float Aspect; }` (ICamera.cs:5).
|
||||
A fixed `DollCamera` impl is trivial.
|
||||
- **RTT infra exists:** `ManagedGLFramebuffer` (color `ITexture` + Depth24Stencil8 renderbuffer,
|
||||
completeness-checked, `GpuMemoryTracker`-tracked) and `GLStateScope` (RAII save/restore of viewport,
|
||||
scissor, depth, cull, blend, program, VAO, FBO bindings, UBO binding 0, …). Both in
|
||||
`src/AcDream.App/Rendering/Wb/`.
|
||||
- **Animated-char pipeline:** `EntitySpawnAdapter.OnCreate(WorldEntity)` (EntitySpawnAdapter.cs:100)
|
||||
requires `ServerGuid != 0`, builds `AnimatedEntityState` via the injected sequencer factory, applies
|
||||
`HiddenPartsMask` + `PartOverrides`, registers each `MeshRef.GfxObjId` + override GfxObj with the mesh
|
||||
adapter (so meshes background-load), and stores `_stateByGuid[ServerGuid]`.
|
||||
- **Player model data is available:** the local player **does** get a fully-resolved `WorldEntity`
|
||||
(`_entitiesByServerGuid[_playerServerGuid]`, GameWindow.cs:12700) with MeshRefs/PaletteOverride/
|
||||
PartOverrides AND an `AnimatedEntityState` (`_animatedEntities[playerEntity.Id]`, `:12805`); its spawn
|
||||
is cached in `_lastSpawnByGuid[_playerServerGuid]`. **(Corrects the handoff's "the local player isn't a
|
||||
WorldEntity" — it is.)** The WorldEntity build template is GameWindow.cs:3390-3431. Appearance updates
|
||||
flow through `OnLiveAppearanceUpdated` (ObjDescEvent `0xF625`, GameWindow.cs:3733).
|
||||
|
||||
---
|
||||
|
||||
## 4. Design
|
||||
|
||||
### A. The Slots toggle — `PaperdollController` extension
|
||||
|
||||
Extend `PaperdollController` (`src/AcDream.App/UI/Layout/PaperdollController.cs`); do not rewrite the
|
||||
slot bindings / wield / unwield.
|
||||
|
||||
- **Partition the existing `SlotMap` by element-id** into the decomp's **9 armor** ids
|
||||
(`0x100005ab/ac/ad/ae/af/b0/b1/b2/b3`) vs the **12 non-armor** ids. (Keyed by element-id, exactly the
|
||||
set the decomp toggles — NOT an `EquipMask` heuristic; HeadWear/HandWear/FootWear are in the armor set,
|
||||
ChestWear/UpperLegWear (shirt/pants) are non-armor.)
|
||||
- Keep refs to the 9 armor-slot `UiItemList`s + the `UiViewport` widget.
|
||||
- Find the Slots button (`0x100005BE`, a `UiButton`) via `layout.FindElement`. On click, flip
|
||||
`_slotView` and apply: doll-view → `viewport.Visible=true`, 9 armor `.Visible=false`; slot-view →
|
||||
`viewport.Visible=false`, 9 armor `.Visible=true`. Non-armor slots untouched.
|
||||
- Default `_slotView=false` (doll-view), mirroring `SetAttribute_Bool(0xe,0)`.
|
||||
|
||||
**AP-66 resolved:** the Slice-1 empty-slot frame **stays** in both views. The corrected model arranges
|
||||
the slots *beside* the doll (the viewport is a ~100-px column), so nothing overlays the doll and nothing
|
||||
flips to transparent. The toggle only changes `Visible`, never the empty-cell art. Reword the AP-66 row.
|
||||
|
||||
### B. The `UiViewport` widget (Type `0xD`)
|
||||
|
||||
New leaf widget `UiViewport : UiElement` (`src/AcDream.App/UI/UiViewport.cs`):
|
||||
|
||||
- Register in `DatWidgetFactory` (`src/AcDream.App/UI/Layout/DatWidgetFactory.cs`, the Type switch):
|
||||
`0xD => new UiViewport(...)`. `ConsumesDatChildren => true` (leaf).
|
||||
- Holds a reference to its **doll scene** (a `UiViewportScene` handle, §C) and the latest rendered
|
||||
color-texture handle. In `OnDraw(UiRenderContext)` it simply **blits that cached handle** as one sprite
|
||||
at its own `ScreenPosition`/`Width`/`Height`. The 3-D render itself happens in the pre-UI hook (§E),
|
||||
*not* in `OnDraw` (which only has a 2-D context). Because the blit is an ordinary sprite in the 2-D
|
||||
pass, it lands in correct painter order (backdrop behind, slots/chrome in front).
|
||||
- When `Visible == false` (slot-view), it draws nothing and the pre-UI hook skips its 3-D pass.
|
||||
|
||||
### C. RTT path + the `IUiViewportRenderer` seam (the crux)
|
||||
|
||||
The 3-D doll renders into an off-screen buffer in the **pre-UI hook** (GameWindow, after the world
|
||||
passes, before `_uiHost.Draw`); the `UiViewport` widget then blits the result during the 2-D UI pass.
|
||||
|
||||
**Seam (layering decision — diverges from the handoff's "Core interface", with user approval):**
|
||||
define `IUiViewportRenderer` in the **`AcDream.App.UI` namespace** (where `UiViewport` lives),
|
||||
implemented by `PaperdollViewportRenderer` in `AcDream.App.Rendering`. `App.UI` is a namespace **within
|
||||
the `AcDream.App` project**, not a separate project — so this is an **intra-App decoupling** (keep the UI
|
||||
widget from depending directly on `WbDrawDispatcher`/`GameWindow`), not a cross-project seam.
|
||||
Code-Structure Rule 2 keeps **Core** free of GL; Core has no consumer for "render an entity into a rect",
|
||||
so a Core interface would over-apply the rule. Narrow surface:
|
||||
|
||||
```csharp
|
||||
// AcDream.App.UI
|
||||
public interface IUiViewportRenderer
|
||||
{
|
||||
// Renders the scene into an internal FBO sized (w,h); returns the GL color-texture handle.
|
||||
// Called by the per-frame pre-UI hook, NOT from UiViewport.OnDraw.
|
||||
uint Render(UiViewportScene scene, int width, int height);
|
||||
}
|
||||
```
|
||||
|
||||
`UiViewportScene` carries the doll entity ref + camera params (plain data; no GL). The pre-UI hook calls
|
||||
`Render` and stores the returned handle on the widget; `UiViewport.OnDraw` blits that handle (§B).
|
||||
|
||||
**`PaperdollViewportRenderer` (App rendering layer)** owns:
|
||||
1. A `ManagedGLFramebuffer` sized to the widget rect, recreated when the rect changes.
|
||||
2. The doll `DollCamera` (`ICamera`): eye `(0.12, −2.4, 0.88)`, look-at origin, perspective with the
|
||||
rect's aspect. Decode/confirm the exact float→matrix mapping against `UIElement_Viewport::SetCamera`.
|
||||
3. Per frame, gated on **inventory-open ∧ doll-view**: open a `GLStateScope`; bind the FBO; set viewport
|
||||
to (w,h); clear color to transparent `(0,0,0,0)` + clear depth; write a **doll lighting UBO** (one
|
||||
`DISTANT_LIGHT` = retail `(0.3,1.9,0.65)`@2.0, fixed ambient) reusing the scene-lighting UBO builder
|
||||
(GameWindow ~8289); call `WbDrawDispatcher.Draw(DollCamera, [dollLandblockTuple],
|
||||
animatedEntityIds:{dollGuid})`; dispose the scope (restores all GL state). Overwriting the shared
|
||||
lighting UBO is safe — nothing 3-D draws after the doll this frame, and GameWindow rebuilds it next
|
||||
frame.
|
||||
|
||||
### D. The doll entity — build, re-dress, animate
|
||||
|
||||
The doll is a **dedicated `WorldEntity`** (retail's `makeObject(player)` clone), distinct from the live
|
||||
player entity (that one is posed/animated in the world):
|
||||
|
||||
- **Build** from `_lastSpawnByGuid[_playerServerGuid]` + `_entitiesByServerGuid[_playerServerGuid]`: same
|
||||
Setup id, `MeshRefs`, `PaletteOverride`, `PartOverrides` — but at the **scene origin**, heading
|
||||
**191.37°**. Assign a **reserved synthetic `ServerGuid`** (non-zero, distinct from every real guid) so
|
||||
`EntitySpawnAdapter.OnCreate` accepts it and registers its meshes. Extract the WorldEntity-build
|
||||
helper from GameWindow.cs:3390-3431 so both paths share it.
|
||||
- **Animate:** route the doll through `EntitySpawnAdapter.OnCreate(dollEntity)` to get a private
|
||||
`AnimatedEntityState`; play the idle animation (`m_didAnimation` analog — the Setup's idle motion).
|
||||
Tick the doll sequencer each frame the doll is visible. Draw via a synthetic one-entry landblock tuple
|
||||
`(dollLandblockId, aabb, [dollEntity], {dollGuid: dollEntity})`.
|
||||
- **Re-dress** (C# `RedressCreature`): hook `OnLiveAppearanceUpdated` for `update.Guid ==
|
||||
_playerServerGuid` to rebuild the doll entity's `PaletteOverride`/`PartOverrides` from the new spawn +
|
||||
refresh the adapter state + re-register meshes. Naked when nothing equipped; re-geared on wield.
|
||||
|
||||
**Idle animation source:** confirm the Setup's idle motion id at impl (retail `m_didAnimation` set by
|
||||
`UpdateForRace` via `DBObj::GetDIDByEnum` per body-type). MVP may use the Setup's default idle; per-race
|
||||
idle DID swap is polish.
|
||||
|
||||
### E. Wiring + lifecycle (GameWindow)
|
||||
|
||||
- Extend the existing `PaperdollController.Bind` call site: also `FindElement(0x100001d5)` for the
|
||||
`UiViewport`, construct `PaperdollViewportRenderer` (closing over `WbDrawDispatcher`, the
|
||||
`EntitySpawnAdapter`, the player spawn/entity accessors, the lighting-UBO builder), give the controller
|
||||
the viewport ref for the toggle, and register the viewport with the per-frame doll-render hook.
|
||||
- **Per-frame:** drive the doll RTT pass only when the **inventory window is open** AND the paperdoll is
|
||||
in **doll-view** AND the player spawn is available. Otherwise no FBO work.
|
||||
- **Teardown:** dispose the FBO + the doll entity's adapter state on logout/teleport (`Clear()`), and
|
||||
rebuild lazily on next show.
|
||||
|
||||
### F. Testing + gate
|
||||
|
||||
- **Unit (App.UI / App.Tests, no GL/dat):** the armor partition is exactly the decomp's 9 ids; toggling
|
||||
flips the correct `Visible` set on {viewport, 9 armor slots} and leaves the 12 non-armor untouched;
|
||||
default = doll-view. `DollCamera` golden View/Projection values. FBO-resize recreation logic
|
||||
(rect-change → new framebuffer). The WorldEntity-build helper produces the expected
|
||||
Setup/PaletteOverride/PartOverrides from a player spawn fixture.
|
||||
- **Visual gate (the acceptance test):** user compares to retail — the doll renders the re-dressed local
|
||||
player (correct race/gender/gear, naked if bare), faces the viewer, idles; the Slots button toggles
|
||||
doll-view ↔ armor-slots; live re-dress on wield/unwield via `0xF625`.
|
||||
- **Full suite green** (Core / Core.Net / App / UI) at the phase boundary, not just filtered subsets
|
||||
([[feedback-ui-resolve-zero-magenta]] process lesson).
|
||||
|
||||
---
|
||||
|
||||
## 5. Component boundaries
|
||||
|
||||
| Unit | Project / file | Responsibility | Depends on |
|
||||
|---|---|---|---|
|
||||
| `PaperdollController` (extend) | `AcDream.App.UI.Layout` | armor/non-armor partition; Slots-button click → toggle `Visible`; hold the viewport ref | `UiButton`, `UiViewport`, `UiItemList` |
|
||||
| `UiViewport` | `AcDream.App.UI` | leaf widget; blit its scene texture at its rect; gate the 3-D pass on `Visible` | `IUiViewportRenderer`, `UiRenderContext` |
|
||||
| `IUiViewportRenderer` + `UiViewportScene` | `AcDream.App.UI` | the narrow UI↔3-D seam (data in, texture handle out) | BCL + Core `WorldEntity` only |
|
||||
| `PaperdollViewportRenderer` | `AcDream.App.Rendering` | own the FBO + `DollCamera` + lighting; run the scissor-free RTT pass via `WbDrawDispatcher` inside a `GLStateScope` | `ManagedGLFramebuffer`, `GLStateScope`, `WbDrawDispatcher`, `EntitySpawnAdapter` |
|
||||
| `DollCamera` | `AcDream.App.Rendering` | fixed `ICamera` from the retail immediates | `ICamera` |
|
||||
| Doll-entity builder | `AcDream.App.Rendering` (shared helper) | build/refresh the doll `WorldEntity` from the player spawn | `WorldEntity`, player spawn/entity accessors |
|
||||
| `DatWidgetFactory` (extend) | `AcDream.App.UI.Layout` | register Type `0xD → UiViewport` | — |
|
||||
|
||||
---
|
||||
|
||||
## 6. Divergence register impact
|
||||
|
||||
- **Reword AP-66** (empty-slot frame): stays in both views (no doll-through / transparent); the toggle
|
||||
changes `Visible` only.
|
||||
- **New rows (as applicable):** per-race camera framing deferred (single default camera); idle-DID
|
||||
per-race swap deferred; the `IUiViewportRenderer` seam placed in App.UI rather than Core (intentional
|
||||
adaptation, with rationale §4C); doll lighting = one distant light written to the shared UBO
|
||||
(approximation iff it differs from retail's `CreatureMode` lighting).
|
||||
- **Gate-verify AP-62** (MissileAmmo `0x100001E0` mask) carried from Slice 1 — opportunistic.
|
||||
|
||||
---
|
||||
|
||||
## 7. Items to pin during the plan (scoped, not open-ended)
|
||||
|
||||
1. `UiButton` click/checkbox API — how `PaperdollController` subscribes to the Slots-button click and
|
||||
reads/sets its checked state.
|
||||
2. The inventory window's **open/closed** query for the per-frame gate (the F12 window manager / the
|
||||
frame's `Visible`).
|
||||
3. The exact `WbDrawDispatcher.Draw` animation contract for a single entity — whether `animatedEntityIds`
|
||||
+ the `AnimatedById` tuple is sufficient, or the doll's `AnimatedEntityState` must also be reachable
|
||||
via the shared `_animatedEntities`/sequencer-tick path; and where the doll sequencer is ticked.
|
||||
4. The scene-lighting UBO layout at GameWindow ~8289 — to write the doll's single distant light.
|
||||
5. `UIElement_Viewport::SetCamera` arg order (pos vs dir) + the exact float→view-matrix construction, so
|
||||
`DollCamera` frames the doll like retail.
|
||||
6. The idle motion id for the player Setup (MVP default vs per-race DID).
|
||||
|
||||
These are mechanism confirmations with identified sources — not unresolved requirements.
|
||||
|
||||
---
|
||||
|
||||
## 8. Acceptance criteria
|
||||
|
||||
- Slots button toggles doll-view (3-D character + 12 non-armor slots) ↔ slot-view (9 armor slots),
|
||||
matching the user's two retail screenshots.
|
||||
- The doll renders the re-dressed local player (race/gender/equipped gear; naked if bare), faces the
|
||||
viewer, idles; updates live on equip/unequip via `0xF625`.
|
||||
- Build + full suite green; **visual gate** passed (user). Divergence rows updated; the seam respects
|
||||
the layering decision in §4C.
|
||||
195
docs/superpowers/specs/2026-06-25-ui-studio-previewer-design.md
Normal file
195
docs/superpowers/specs/2026-06-25-ui-studio-previewer-design.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# acdream UI Studio — design (live previewer + inspector)
|
||||
|
||||
- Date: 2026-06-25
|
||||
- Status: approved (user pre-approved full v1 scope 2026-06-25)
|
||||
- Branch: claude/hopeful-maxwell-214a12
|
||||
- Supersedes/foundation for: the deferred phase-2 drag-drop *designer*
|
||||
|
||||
## Goal
|
||||
|
||||
A standalone, fast-booting dev tool that renders **any acdream UI panel** through the
|
||||
**production renderer**, with a click-to-inspect element inspector, sample-data fixtures,
|
||||
markup hot-reload + write-back editing, and render-config sliders.
|
||||
|
||||
It collapses the panel-iteration loop — today `edit → build → login to ACE → wait ~8s →
|
||||
F12 → eyeball vs retail → close → repeat` (~30–60 s/iteration) — into `edit → glance`
|
||||
(~1 s). Everything we did on the paperdoll this session (confirming the pose, framing the
|
||||
camera, catching the mirrored heading) would have been a few clicks here instead of a dozen
|
||||
relaunches.
|
||||
|
||||
**Fidelity is the whole point.** The previewed panel is drawn by the SAME
|
||||
`UiHost` / `LayoutImporter` / `DatWidgetFactory` / dat-sprite path that ships, so what you
|
||||
see is what the game renders. This is the decisive reason NOT to build the tool in a
|
||||
separate stack (Godot/HTML) and NOT to rely on the existing `render-vitals-mockup` (a
|
||||
separate CPU `SurfaceDecoder` composite that can drift from the GL path).
|
||||
|
||||
## Scope — v1 is the maximum capability
|
||||
|
||||
IN:
|
||||
- Load a panel from EITHER a dat `LayoutDesc` id OR a markup file (`MarkupDocument`).
|
||||
- Render via the production GL `UiHost` (2-D panels) AND the full WB mesh pipeline (the
|
||||
3-D paperdoll **doll**).
|
||||
- **Sample-data fixtures** so data-bound panels look populated: canned inventory items,
|
||||
sample vital values, a dressed sample creature for the doll.
|
||||
- **Inspector (ImGui):** element tree, click-to-inspect, read-only props
|
||||
(id / Type / rect / anchors / state-sprites / ZOrder), **editable** markup props
|
||||
(write back to the file), and **render-config sliders** (the doll camera eye / FOV /
|
||||
heading — values that live in code, not markup).
|
||||
- **Hot-reload:** file-watch the markup source; reload-on-demand (id picker / reload button)
|
||||
for dat layouts.
|
||||
|
||||
OUT (phase 2): the drag-drop visual **designer** (compose *new* layouts on a canvas and
|
||||
emit markup). The previewer is the foundation that phase sits on. Its natural home is the
|
||||
plugin-UI story, not the retail-panel work.
|
||||
|
||||
Not faithfulness-gated: this is a DEV tool. The previewed *panels* are faithful (same
|
||||
renderer); the tool *chrome* (the ImGui inspector) is dev-only. No
|
||||
`retail-divergence-register` rows are created by this work.
|
||||
|
||||
## Architecture
|
||||
|
||||
Seven isolated, single-purpose units. The previewed panel is drawn by the production
|
||||
renderer; the tool chrome is **ImGui** (already in `AcDream.App` for the `ACDREAM_DEVTOOLS`
|
||||
overlay — ideal for a tree / sliders / editable fields at near-zero cost).
|
||||
|
||||
```
|
||||
Program.cs ──"ui-studio"──▶ StudioWindow
|
||||
│ owns: Silk window, ImGui ctx, frame loop
|
||||
▼
|
||||
RenderBootstrap.Create(gl, opts) ──▶ RenderStack
|
||||
│ (GL + DatCollection + TextureCache + UiHost
|
||||
│ + ObjectMeshManager + WbDrawDispatcher + lighting UBO)
|
||||
│ ALSO consumed by GameWindow (shared = de-tangles OnLoad)
|
||||
┌─────────────────────┼───────────────────────────────┐
|
||||
▼ ▼ ▼
|
||||
LayoutSource FixtureProvider StudioInspector (ImGui)
|
||||
dat id | markup sample item table / vitals / tree · props(read+edit) ·
|
||||
→ UiElement tree dressed creature → controllers render sliders · canvas(FBO)
|
||||
│ │ click → HitTestTopDown
|
||||
└────────── HotReloadWatcher (markup) ─────────────────┘
|
||||
MarkupWriteBack (edited props → file)
|
||||
```
|
||||
|
||||
### 1. `RenderBootstrap` — NEW, `src/AcDream.App/Rendering/`
|
||||
Extracts the **render-stack construction** currently tangled inside `GameWindow.OnLoad`
|
||||
(the GL api, dat-dir → `DatCollection`, `TextureCache`, the WB mesh pipeline —
|
||||
`ObjectMeshManager` / `WbDrawDispatcher` / `SceneLightingUboBinding` — and `UiHost`) into a
|
||||
reusable unit that BOTH `GameWindow` and `StudioWindow` build through.
|
||||
- Interface: `RenderStack Create(GL gl, RenderBootstrapOptions opts)`; `RenderStack` is a
|
||||
record of the constructed pieces (dats, textureCache, meshManager, drawDispatcher,
|
||||
lightingUbo, uiHost, shaderDir).
|
||||
- `GameWindow.OnLoad` is refactored to consume it — **behavior-preserving**, locked by the
|
||||
existing App/Core suites + a visual gate that the GAME still renders.
|
||||
- Highest-risk step → extract the SMALLEST cohesive slice only; do it first, gate it, build
|
||||
the studio on it second. Fallback if extraction is too entangled: `StudioWindow` builds a
|
||||
studio-local duplicate of the mesh-pipeline setup (more drift, lower regression risk) — to
|
||||
be decided at implementation, not assumed away.
|
||||
|
||||
### 2. `StudioWindow` — NEW, `src/AcDream.App/Studio/`
|
||||
A Silk.NET windowed GL app — the studio entry. Owns the window, the `RenderStack`, the ImGui
|
||||
context, the inspector, the hot-reload watcher, and the per-frame loop. **No** ACE /
|
||||
world-streaming / physics / movement input. Launched from `Program.cs`:
|
||||
`AcDream.App ui-studio <datdir> [--layout 0x21000023 | --markup path.xml]`. Config flows
|
||||
through `RuntimeOptions` (one env/arg read), per the code-structure rules.
|
||||
|
||||
### 3. `LayoutSource` — NEW, `src/AcDream.App/Studio/`
|
||||
Loads a panel into a `UiElement` subtree from a dat id (`LayoutImporter.Import`) or a markup
|
||||
file (`MarkupDocument.Build`). Owns the current source + `Reload()`.
|
||||
- Interface: `UiElement Load()`, `void Reload()`, `SourceKind Kind`, `string? MarkupPath`,
|
||||
`uint? LayoutId`, plus a `string? LastError` for the inspector to surface parse failures.
|
||||
|
||||
### 4. `FixtureProvider` — NEW, `src/AcDream.App/Studio/`
|
||||
Supplies sample data so data-bound panels render populated. A fake `ClientObjectTable`
|
||||
seeded with a handful of canned items (icon ids + types); sample vital values; a sample
|
||||
creature (Setup + ObjDesc) for the doll. Maps a known `layoutId` → its real controller
|
||||
(`VitalsController` / `InventoryController` / `ToolbarController` / `PaperdollController`)
|
||||
bound to the fixtures — REUSING the production controllers, not re-implementing them.
|
||||
- Interface: `void Populate(uint layoutId, UiElement root, RenderStack stack)`. Unknown
|
||||
layout id → no fixture (structural preview only), not an error.
|
||||
|
||||
### 5. `StudioInspector` — NEW, `src/AcDream.App/Studio/`
|
||||
The ImGui tool chrome, four docked windows:
|
||||
- **Canvas** — the panel rendered to an FBO (RTT, the `PaperdollViewportRenderer` technique
|
||||
applied to the whole panel) shown via `ImGui.Image`. Click → map canvas-local coords →
|
||||
`UiRoot.HitTestTopDown` → select. The FBO route gives a clean dockable IDE layout +
|
||||
accurate click mapping, and isolates the panel's GL state from ImGui's.
|
||||
- **Tree** — the live `UiElement` hierarchy with each node's registered Type; select syncs
|
||||
with the canvas highlight.
|
||||
- **Properties** — the selected element's id / Type / rect / anchors / state-sprites (with
|
||||
swatches) / ZOrder. Rect/anchor fields are **editable** (markup source) → applied live +
|
||||
queued to `MarkupWriteBack`.
|
||||
- **Render config** — sliders for the doll's `DollCamera` eye / FOV / heading, mutating the
|
||||
live camera so the doll updates next frame; a "copy values" button emits the literals to
|
||||
paste into code.
|
||||
|
||||
### 6. `MarkupWriteBack` — NEW, `src/AcDream.App/Studio/`
|
||||
Serializes an edited element's changed props back to the markup XML file (markup source
|
||||
only; dat layouts are read-only). v1 = **targeted in-place attribute updates** (find the
|
||||
element by id, rewrite the changed attribute values), NOT a full tree re-serialize — so
|
||||
hand-authored formatting/comments survive.
|
||||
|
||||
### 7. `HotReloadWatcher` — NEW, `src/AcDream.App/Studio/`
|
||||
`FileSystemWatcher` on the markup file → debounced (≈150 ms) reload event → `LayoutSource.Reload`
|
||||
→ `FixtureProvider.Populate` → re-select the previously-selected element by id when possible.
|
||||
|
||||
## Data flow
|
||||
|
||||
- **Boot:** `StudioWindow` → `RenderBootstrap.Create` → `LayoutSource.Load(initial)` →
|
||||
`FixtureProvider.Populate` → loop.
|
||||
- **Frame:** bind panel FBO → `UiHost.Tick` + `Draw` (panel, incl. the doll's own nested
|
||||
RTT) → unbind → ImGui frame (Canvas image + Tree + Properties + Render-config) → present.
|
||||
- **Click canvas:** map coords → `HitTestTopDown` → select → Properties shows props.
|
||||
- **Edit prop:** apply to the live `UiElement` (instant) + (markup) queue write-back.
|
||||
- **File change:** watcher → `Reload` → `Populate` → re-select by id.
|
||||
- **Render slider:** mutate `DollCamera` → next frame the doll re-renders.
|
||||
|
||||
## Error handling
|
||||
|
||||
- Bad layout id / unparseable markup → `LayoutSource.LastError` shown in the inspector; the
|
||||
previous panel stays (or an empty canvas), no crash.
|
||||
- Missing dats at boot → clear error + exit non-zero.
|
||||
- Panel build/draw wrapped in try/catch → a thrown panel surfaces its exception text in the
|
||||
inspector; the tool stays alive (you're often previewing half-broken markup).
|
||||
- Write-back failure (file locked) → non-fatal inline notice, edit stays applied in-memory.
|
||||
|
||||
## Testing
|
||||
|
||||
- `LayoutSource`: load a known dat id + a markup string → assert the `UiElement` tree shape
|
||||
(reuse the existing golden-fixture pattern, e.g. `vitals_2100006C.json`).
|
||||
- `FixtureProvider`: assert the seeded `ClientObjectTable` + vitals shapes.
|
||||
- `MarkupWriteBack`: round-trip — parse → edit a rect → serialize → re-parse → assert the
|
||||
changed attribute + that siblings/formatting are untouched.
|
||||
- `RenderBootstrap`: the existing suites stay green + a manual game visual gate (the refactor
|
||||
is behavior-preserving).
|
||||
- Window / ImGui / doll render: manual visual gate — the studio is itself the visual tool.
|
||||
- Tests live in `tests/AcDream.App.Tests/Studio/`.
|
||||
|
||||
## Build order (staged; each ≈ one subagent chunk + a gate)
|
||||
|
||||
1. **`RenderBootstrap` extraction** — behavior-preserving. GATE: game renders unchanged;
|
||||
suites green.
|
||||
2. **`StudioWindow` skeleton + `LayoutSource`(dat id)** — render a 2-D panel to the window.
|
||||
GATE: `ui-studio --layout 0x2100006C` shows vitals.
|
||||
3. **`StudioInspector`** — ImGui + canvas-FBO + tree + read-only props + click-to-inspect.
|
||||
GATE: click an element → see its id/rect/sprites.
|
||||
4. **`FixtureProvider`** — 2-D panels populated (vitals / toolbar / inventory). GATE:
|
||||
inventory shows sample items.
|
||||
5. **Live doll** — `PaperdollViewportRenderer` in the studio + sample creature. GATE: the
|
||||
paperdoll shows a dressed doll.
|
||||
6. **Markup source + `HotReloadWatcher`** — GATE: edit a markup file → studio reloads.
|
||||
7. **Editable markup props + `MarkupWriteBack`** — GATE: edit a rect in the inspector → file
|
||||
updates → reloads.
|
||||
8. **Render-config sliders** — GATE: drag the eye/FOV → doll updates live.
|
||||
|
||||
Steps 1–4 deliver the high-value 2-D previewer + inspector + fixtures fast; 5–8 add the doll
|
||||
and the editing power. The end-state is the full maximum-capability tool.
|
||||
|
||||
## Open risks
|
||||
|
||||
- **`RenderBootstrap` extraction from the tangled `GameWindow.OnLoad`** is the riskiest step.
|
||||
Mitigation: smallest cohesive slice, behavior-preserving, gated by the game visual + the
|
||||
suites BEFORE the studio depends on it; documented fallback (studio-local duplicate).
|
||||
- **`MarkupWriteBack` formatting preservation** — keep v1 to in-place attribute edits, never
|
||||
a full re-serialize, so hand-authored markup isn't clobbered.
|
||||
- **ImGui + UiHost GL-state interplay** — the panel renders to its own FBO and ImGui owns the
|
||||
default framebuffer, so their GL state can't fight (the same seal the paperdoll RTT uses).
|
||||
Loading…
Add table
Add a link
Reference in a new issue