Bite-sized, gated build order: (1) RenderBootstrap [new code, no GameWindow edit → zero game-regression risk], (2) StudioWindow + LayoutSource + ui-studio subcommand, (3) ImGui inspector (canvas FBO + tree + props + click-inspect), (4) FixtureProvider sample data, (5) live 3-D doll, (6) markup + hot-reload, (7) editable props + write-back, (8) doll-camera sliders. Pure-logic units (LayoutSource/FixtureProvider/MarkupWriteBack) unit-tested; GL/window tasks visual-gated. Spec: docs/superpowers/specs/2026-06-25-ui-studio-previewer-design.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
26 KiB
acdream UI Studio Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a standalone Silk.NET dev tool that renders any acdream UI panel through the production render stack, with an ImGui click-to-inspect inspector, sample-data fixtures, the live 3-D doll, markup hot-reload + write-back, and doll-camera sliders.
Architecture: A new StudioWindow (Silk GL app, launched via a ui-studio subcommand in Program.cs) builds its render stack through a new RenderBootstrap (a clean construction of just the subset the studio needs — GL, DatCollection, TextureCache, the WB mesh pipeline, UiHost — leaving GameWindow untouched). A LayoutSource loads a panel (dat LayoutDesc id or markup file) into a UiElement tree; a FixtureProvider populates it via the production controllers; a StudioInspector (ImGui) renders the tool chrome with the panel shown as an FBO image.
Tech Stack: C# .NET 10, Silk.NET (Windowing/Input/OpenGL), ImGui.NET 1.91.6.1 + Silk.NET.OpenGL.Extensions.ImGui 2.23.0 (via the existing AcDream.UI.ImGui project), the existing AcDream.App.UI retained-mode tree + LayoutImporter/DatWidgetFactory, DatReaderWriter.
Spec: docs/superpowers/specs/2026-06-25-ui-studio-previewer-design.md
Subagent note (project policy): implementers are Sonnet and INHERIT CLAUDE.md; they read the cited code themselves. Tasks give the contract, the key code, the files to read, the test, the gate, and the commit message — not every line. GL/window tasks verify via a visual gate (the studio is itself the visual tool); pure-logic tasks (LayoutSource infos, FixtureProvider, MarkupWriteBack) use real unit tests.
Branch: claude/hopeful-maxwell-214a12. Commit after every task. Full build (dotnet build src/AcDream.App/AcDream.App.csproj) green before any commit; relevant tests green.
Launch (for every visual gate):
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug -- ui-studio "$env:USERPROFILE\Documents\Asheron's Call" --layout 0x2100006C
Do NOT auto-kill a running client; if a rebuild is locked, ask the user to close it (feedback_dont_kill_clients_before_launch).
File structure
New, all under src/AcDream.App/:
Rendering/RenderBootstrap.cs—RenderStack Create(GL, RenderBootstrapOptions); the shared render-stack builder + theRenderStackrecord.Studio/StudioWindow.cs— the Silk window + frame loop + owns the stack/inspector/sources.Studio/StudioOptions.cs— parsedui-studioargs (layout id | markup path | dat dir).Studio/LayoutSource.cs— load/reload a panel from a dat id or a markup file.Studio/FixtureProvider.cs— sample data + per-layout controller binding.Studio/SampleData.cs— the canned items / vitals / creature fixture constants.Studio/StudioInspector.cs— the ImGui chrome (canvas/tree/properties/render-config).Studio/PanelFbo.cs— render-to-texture target for the previewed panel (mirrorsPaperdollViewportRenderer's FBO).Studio/MarkupWriteBack.cs— targeted in-place markup attribute edits.Studio/HotReloadWatcher.cs— debouncedFileSystemWatcher.- Modify:
src/AcDream.App/Program.cs—ui-studiosubcommand dispatch.
Tests under tests/AcDream.App.Tests/Studio/:
LayoutSourceTests.cs,FixtureProviderTests.cs,MarkupWriteBackTests.cs.
Task 1: RenderBootstrap (shared render stack)
Files:
- Create:
src/AcDream.App/Rendering/RenderBootstrap.cs - Read first:
src/AcDream.App/Rendering/GameWindow.cs:1026-1815,2313-2408(the construction order),src/AcDream.App/UI/UiHost.cs,src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs(ctor),src/AcDream.App/UI/Layout/UiDatFont.cs.
Contract:
namespace AcDream.App.Rendering;
public sealed record RenderStack(
GL Gl,
DatReaderWriter.DatCollection Dats,
string ShaderDir,
BindlessSupport Bindless,
TextureCache TextureCache,
Shader MeshShader,
Wb.WbMeshAdapter MeshAdapter,
Wb.EntitySpawnAdapter EntitySpawnAdapter,
Wb.WbDrawDispatcher DrawDispatcher,
SceneLightingUboBinding LightingUbo,
AcDream.App.UI.UiHost UiHost,
AcDream.App.UI.Layout.UiDatFont? VitalsDatFont)
{
/// <summary>sprite id (0x06xxxxxx) → (GL handle, width, height) for the importer's resolve func.</summary>
public (uint, int, int) ResolveChrome(uint spriteId)
=> TextureCache.GetOrUploadRenderSurface(spriteId, out var w, out var h) is var h2
? (h2, w, h) : (0u, 0, 0); // match GameWindow.ResolveChrome — read its exact body first
}
public sealed record RenderBootstrapOptions(QualitySettings Quality);
public static class RenderBootstrap
{
/// <summary>Build the subset of the production render stack the UI Studio needs — same classes,
/// same construction order as GameWindow.OnLoad, minus terrain/sky/physics/streaming. Throws
/// NotSupportedException if bindless/draw-params are missing (same as the game).</summary>
public static RenderStack Create(GL gl, DatReaderWriter.DatCollection dats, RenderBootstrapOptions opts);
}
- Step 1: Read the cited GameWindow construction lines and
ResolveChrome(grep it) so the order + the exactGetOrUploadRenderSurface/sequencer-factory signatures are copied, not guessed. - Step 2: Write
RenderBootstrap.Createconstructing, in order:BindlessSupport.TryCreate(throw if absent/no draw-params, copy the GameWindow message),shadersDir = Path.Combine(AppContext.BaseDirectory,"Rendering","Shaders"),SceneLightingUboBinding(gl),MeshShader = new Shader(gl, mesh_modern.vert/.frag),TextureCache(gl, dats, bindless),WbMeshAdapter(gl, dats, NullLogger<WbMeshAdapter>.Instance), theSequencerFactorylambda (copy from GameWindow:2335-2361, capturingdats+ anAnimLoader),EntitySpawnAdapter(textureCache, sequencerFactory, meshAdapter),WbDrawDispatcher(gl, meshShader, textureCache, meshAdapter, entitySpawnAdapter, bindless, new <classificationCache>)withAlphaToCoverage = opts.Quality.AlphaToCoverage, the vitals dat font (copy GameWindow's load of0x40000000viaUiDatFont), andUiHost(gl, shadersDir, defaultFont). Return theRenderStack. - Step 3: Build:
dotnet build src/AcDream.App/AcDream.App.csproj -c Debug. Expected: green (no test yet — GL construction is verified by Task 2's boot gate). - Step 4: Commit.
Gate: Build green. (Functional gate is Task 2.)
Commit: feat(studio): RenderBootstrap — shared render stack for the UI previewer
Task 2: StudioWindow + LayoutSource (dat id) + Program dispatch
Files:
- Create:
src/AcDream.App/Studio/StudioOptions.cs,src/AcDream.App/Studio/LayoutSource.cs,src/AcDream.App/Studio/StudioWindow.cs - Modify:
src/AcDream.App/Program.cs(beforenew GameWindow) - Test:
tests/AcDream.App.Tests/Studio/LayoutSourceTests.cs - Read first:
src/AcDream.App/UI/Layout/LayoutImporter.cs:57-190,src/AcDream.App/Rendering/GameWindow.cs:972-1017(window+Run),src/AcDream.App/UI/UiHost.cs:65-96(Draw/WireMouse/WireKeyboard).
StudioOptions contract:
namespace AcDream.App.Studio;
public sealed record StudioOptions(string DatDir, uint? LayoutId, string? MarkupPath)
{
public static StudioOptions Parse(string[] args); // args AFTER the "ui-studio" token
}
Parse: positional dat dir (or ACDREAM_DAT_DIR), --layout 0xNNNN (hex), --markup <path>. Default layout when neither given: 0x2100006Cu (vitals).
LayoutSource contract:
public enum LayoutSourceKind { DatLayout, Markup }
public sealed class LayoutSource
{
public LayoutSource(DatCollection dats, Func<uint,(uint,int,int)> resolve, UiDatFont? datFont);
public LayoutSourceKind Kind { get; }
public uint? LayoutId { get; }
public string? MarkupPath { get; }
public string? LastError { get; private set; }
/// <summary>Imported tree, or null on error (LastError set). Dat → LayoutImporter.Import; markup → MarkupDocument.Build with an empty binding.</summary>
public ImportedLayout? CurrentLayout { get; private set; }
public UiElement? Load(StudioOptions opts); // sets Kind/LayoutId/MarkupPath + CurrentLayout
public UiElement? Reload(); // re-run the current source; markup re-reads the file
}
- Step 1 (test, GL-free):
LayoutSourceTests.LoadsDatLayout_byId. Open real dats viaConformanceDats.ResolveDatDir()(skip if null), build aLayoutSourcewith a STUB resolve_ => (1u,1,1)+ null font,Load(new StudioOptions(dir, 0x2100006Cu, null)), assertCurrentLayoutnon-null andFindElement(0x2100006Cu) != nullandKind == DatLayout. (UseLayoutImporter.ImportInfosis GL-free;Import's resolve is only called for sprites — the stub is fine for tree-shape.) - Step 2: Run it — expect FAIL (LayoutSource missing).
dotnet test tests/AcDream.App.Tests/AcDream.App.Tests.csproj --filter LayoutSourceTests. - Step 3: Implement
StudioOptions.Parse+LayoutSource(dat path only; markup path returns LastError "markup unsupported" for now — wired in Task 6). Dat path =LayoutImporter.Import(dats, id, resolve, datFont). - Step 4: Run the test — expect PASS.
- Step 5: Implement
StudioWindow: mirrorGameWindow.Run()window creation (1280×720, GL 4.3 Core, stencil 8). OnLoad:gl = GL.GetApi(window),input = window.CreateInput(),dats = new DatCollection(opts.DatDir, DatAccessType.Read),stack = RenderBootstrap.Create(gl, dats, new(QualitySettings...)), wirestack.UiHost.WireMouse/WireKeyboardfor each mouse/keyboard,source = new LayoutSource(dats, stack.ResolveChrome, stack.VitalsDatFont),root = source.Load(opts), and if non-null add it tostack.UiHost.Root(use the same add-child API GameWindow uses — grep_uiHost.Rootadds). OnRender:gl.ClearColormid-grey,Clear,stack.UiHost.Tick(dt)+stack.UiHost.Draw(new Vector2(window.Size.X, window.Size.Y)). OnClosing: dispose. - Step 6:
Program.cs: beforenew GameWindow, addif (args.Length >= 1 && args[0] == "ui-studio") { var so = AcDream.App.Studio.StudioOptions.Parse(args[1..]); using var sw = new AcDream.App.Studio.StudioWindow(so); sw.Run(); return 0; }. - Step 7: Build green, then VISUAL GATE (launch command above). Expect: a window showing the vitals panel chrome (three empty bars + frame) rendered by the real UiHost.
- Step 8: Commit.
Gate (visual): ui-studio --layout 0x2100006C opens a window showing the vitals panel.
Commit: feat(studio): StudioWindow + LayoutSource — render a dat panel standalone
Task 3: StudioInspector (ImGui) + canvas FBO + tree + props + click-to-inspect
Files:
- Create:
src/AcDream.App/Studio/PanelFbo.cs,src/AcDream.App/Studio/StudioInspector.cs - Modify:
StudioWindow.cs(init ImGui, render panel→FBO, draw inspector) - Read first:
src/AcDream.UI.ImGui/ImGuiBootstrapper.cs:35-46,src/AcDream.App/Rendering/PaperdollViewportRenderer.cs(the FBO RTT pattern +GLStateScope),src/AcDream.App/UI/UiRoot.cs(grepHitTestTopDown),src/AcDream.App/UI/UiElement.cs(Id/Type/Left/Top/Width/Height/Anchors/ZOrder/Children accessors).
PanelFbo: mirror PaperdollViewportRenderer's FBO (RGBA8 + Depth24Stencil8, lazy resize). uint Render(int w, int h, UiHost host) = bind FBO, viewport, clear transparent, host.Draw(new Vector2(w,h)) inside a GLStateScope, return color tex. Reuse the exact FBO setup from PaperdollViewportRenderer.EnsureFramebuffer.
StudioInspector: ImGui windows, drawn between BeginFrame/Render:
public sealed class StudioInspector
{
public UiElement? Selected { get; set; }
// Canvas window: ImGui.Image(panelTex) sized to the panel; capture click → return panel-local (x,y)
public (int x,int y)? DrawCanvas(nint panelTex, int w, int h);
public void DrawTree(UiElement root); // recursive ImGui.TreeNode; click sets Selected
public void DrawProperties(); // Selected's Id/Type/Rect/Anchors/ZOrder/sprites (read-only here)
}
Click-to-inspect: when DrawCanvas returns a coord, call root HitTestTopDown(x,y) (grep its exact signature/return) → set Selected.
- Step 1: Implement
PanelFbo(copyPaperdollViewportRendererFBO lifecycle; swap the doll draw forhost.Draw). - Step 2:
StudioWindow.Load: constructnew ImGuiBootstrapper(gl, window, input)(noDevToolsEnabledgate — the studio is always devtools) +new PanelFbo(gl)+new StudioInspector(). - Step 3:
StudioWindow.Render:panelTex = panelFbo.Render(w,h, stack.UiHost);imgui.BeginFrame(dt);var click = inspector.DrawCanvas((nint)panelTex, w, h); if (click is {} c) inspector.Selected = root.HitTestTopDown(c.x, c.y); inspector.DrawTree(root); inspector.DrawProperties();;imgui.Render(). - Step 4: Implement
StudioInspectorwindows (ImGui.NET:ImGui.Begin,ImGui.Image,ImGui.TreeNodeEx,ImGui.Text). Tree recurseselement.Children; each node label =$"{name} 0x{Id:X8} [{Type}]". Properties: Id, Type, Rect(Left,Top,Width,Height), Anchors, ZOrder, and each state sprite id (grep howUiDatElement/UiButtonexpose their sprite ids; if not trivially exposed, list "—" for v1 and note follow-up). - Step 5: Build green, VISUAL GATE: window shows a Canvas pane (the panel) + a Tree pane + a Properties pane; clicking the panel or a tree node selects an element and shows its id/rect.
- Step 6: Commit.
Gate (visual): click an element in the canvas or tree → its id / Type / rect appear in Properties.
Commit: feat(studio): ImGui inspector — canvas FBO, element tree, props, click-to-inspect
Task 4: FixtureProvider — populate 2-D panels with sample data
Files:
- Create:
src/AcDream.App/Studio/SampleData.cs,src/AcDream.App/Studio/FixtureProvider.cs - Modify:
StudioWindow.cs(callFixtureProvider.PopulateafterLoad) - Test:
tests/AcDream.App.Tests/Studio/FixtureProviderTests.cs - Read first:
src/AcDream.Core/Items/ClientObjectTable.cs:41-420(AddOrUpdate/Get/GetContents/ClientObjectfields), the four controllers'Bind(signatures in the plan header research),src/AcDream.Core/Items/ItemType.cs+EquipMask.
SampleData: static factory building a ClientObjectTable seeded with: a player object (guid 0xPLAYER, ItemsCapacity=102, ContainersCapacity=7), ~6 loose items in the main pack (varied Type/IconId — pick real icon ids from existing tests or a known wcid; ContainerId = player, gapless ContainerSlot), ~2 side bags (Type=Container, ItemsCapacity>0), and a few equipped items (CurrentlyEquippedLocation set to Head/Chest/etc). Plus sample vitals (health=0.8f, stam=0.6f, mana=0.9f + text). Constants only — no wire.
FixtureProvider.Populate(uint layoutId, ImportedLayout layout, RenderStack stack) switch on layoutId:
-
0x2100006C→VitalsController.Bind(layout, ()=>0.8f, ()=>0.6f, ()=>0.9f, ()=>"80/100", ()=>"60/100", ()=>"90/100"). -
0x21000016→ToolbarController.Bind(layout, sampleTable, ()=>PLAYER, IconIds, _=>{}, ...digits from GameWindow's load...). -
0x21000023→InventoryController.Bind(layout, sampleTable, ()=>PLAYER, IconIds, ()=>100, stack.VitalsDatFont, contents/sideBag/mainPack sprites, ...). -
default → no-op (structural preview).
IconIds= reuse GameWindow'siconIdslambda (grep it —IconComposer-backed); if heavy, a stub returning the passed base id is acceptable for v1 (note it). -
Step 1 (test):
FixtureProviderTests.SampleTable_hasPackContents. BuildSampleData.BuildObjectTable(), asserttable.GetContents(PLAYER).Count >= 6and a known item'sType/IconIdset. -
Step 2: Run — FAIL (missing).
-
Step 3: Implement
SampleData+ the table seeding. -
Step 4: Run — PASS.
-
Step 5: Implement
FixtureProvider.Populate; call it fromStudioWindowaftersource.Load. -
Step 6: Build green, VISUAL GATE:
--layout 0x21000023shows the inventory with sample items in the grid + a non-zero burden bar;--layout 0x21000016shows toolbar slots populated;--layout 0x2100006Cshows filled vital bars. -
Step 7: Commit.
Gate (visual): inventory / toolbar / vitals panels render POPULATED (sample items, filled bars).
Commit: feat(studio): FixtureProvider — sample data populates the 2-D panels
Task 5: Live doll in the paperdoll viewport
Files:
- Modify:
FixtureProvider.cs(build a sample creature entity for0x21000023/paperdoll),StudioWindow.cs(wirePaperdollViewportRenderer+ the pre-UI doll render, mirroring GameWindow:9148-9160) - Read first:
src/AcDream.App/Rendering/GameWindow.cs:3786-3897(RefreshPaperdollDoll/ApplyPaperdollPose),9148-9160(the pre-UI doll hook),src/AcDream.App/Rendering/DollEntityBuilder.cs(Build),src/AcDream.App/Rendering/PaperdollViewportRenderer.cs(ctor +SetDoll/Render),EntitySpawnAdapter.OnCreate.
Sample creature: a base human Setup id (grep the test character / a known humanoid Setup 0x0200....; or reuse the player Setup constant if one exists in tests). Build a WorldEntity { SourceGfxObjOrSetupId = setupId, MeshRefs = <resolved from Setup via WbMeshAdapter>, ... }, run it through stack.EntitySpawnAdapter.OnCreate to resolve MeshRefs (same path GameWindow uses), then DollEntityBuilder.Build + ApplyPaperdollPose-equivalent. Keep a small StudioDoll helper if this grows.
- Step 1: In
StudioWindow.Load, constructnew PaperdollViewportRenderer(gl, stack.DrawDispatcher, stack.LightingUbo). - Step 2: When the layout is the inventory/paperdoll, have
FixtureProviderbuild the sample creature, callRefreshPaperdollDoll-equivalent (clone →ApplyPaperdollPose) →paperdollRenderer.SetDoll(doll); expose the doll-viewport widget (FindElement0x100001D5). - Step 3: In
StudioWindow.Render, BEFORE the panel FBO draw, render the doll into the viewport widget's texture exactly as GameWindow:9156-9159 (widget.TextureHandle = paperdollRenderer.Render(w,h)), gated on the viewport widget being present + visible. - Step 4: Build green, VISUAL GATE:
--layout 0x21000023shows the paperdoll with a DRESSED 3-D doll in the held pose (reuses the camera/heading/pose shipped in8fa66c2). - Step 5: Commit.
Gate (visual): the paperdoll viewport shows the sample creature as a posed 3-D doll.
Commit: feat(studio): live 3-D doll in the paperdoll preview
Task 6: Markup source + hot-reload
Files:
- Create:
src/AcDream.App/Studio/HotReloadWatcher.cs - Modify:
LayoutSource.cs(markup path viaMarkupDocument.Build),StudioWindow.cs(watch + reload),StudioOptionsalready parses--markup - Read first:
src/AcDream.App/UI/MarkupDocument.cs:21-82, an existing first-party markup file (grepassets/*.xml, e.g. the retiredvitals.xmlin git history or a current plugin panel) for the schema. - Test: extend
LayoutSourceTests.
LayoutSource markup path: Kind=Markup; Load reads the file → MarkupDocument.Build(xml, EmptyBinding, resolve, null) → wrap its UiNineSlicePanel as the root; Reload re-reads the file. EmptyBinding = new object() (no {Binding} resolution needed for preview; unresolved bindings render literally or empty — acceptable).
HotReloadWatcher:
public sealed class HotReloadWatcher : IDisposable
{
public HotReloadWatcher(string filePath, Action onChanged); // debounce ~150ms; marshal onChanged via a thread-safe flag the window polls
}
The watcher sets a volatile bool _dirty; StudioWindow.Render checks it and calls source.Reload() + FixtureProvider.Populate + re-add to the UiRoot on the GL thread (FileSystemWatcher fires on a worker thread — never touch GL off-thread).
- Step 1 (test):
LayoutSourceTests.LoadsMarkupFile. Write a temp.xmlwith a minimalUiNineSlicePanel,Load(new StudioOptions(dir, null, path)), assert non-null root +Kind==Markup. - Step 2: Run — FAIL.
- Step 3: Implement the markup path in
LayoutSource+HotReloadWatcher. - Step 4: Run — PASS.
- Step 5: Wire the watcher in
StudioWindow(poll_dirtyin Render; reload on the GL thread). - Step 6: Build green, VISUAL GATE: launch
--markup <path>; edit the file in an editor + save → the studio reloads the panel within ~1s. - Step 7: Commit.
Gate (visual): edit a markup file externally → studio re-renders it live.
Commit: feat(studio): markup source + hot-reload
Task 7: Editable markup props + write-back
Files:
- Create:
src/AcDream.App/Studio/MarkupWriteBack.cs - Modify:
StudioInspector.cs(editable rect/anchor fields whenKind==Markup),StudioWindow.cs(apply edit live + persist) - Test:
tests/AcDream.App.Tests/Studio/MarkupWriteBackTests.cs
MarkupWriteBack: targeted in-place attribute edits — NOT a full re-serialize (preserve formatting/comments):
public static class MarkupWriteBack
{
/// <summary>In the markup text, find the element whose id attribute == elementId and set attr=value
/// (add the attribute if absent). Returns the new text. Leaves all other bytes untouched.</summary>
public static string SetAttribute(string markup, uint elementId, string attr, string value);
}
Implement by locating the element's opening tag (match the id attribute literal Id="0x...." — grep the markup schema for the exact id attribute name), then regex-replace just that attribute within that tag span.
- Step 1 (test):
MarkupWriteBackTests.SetAttribute_updatesInPlace. Given a 2-element markup string,SetAttribute(m, id, "Width", "200")→ re-parse, assert the target's Width==200 AND the sibling element + any comment are byte-identical. - Step 2: Run — FAIL.
- Step 3: Implement
MarkupWriteBack.SetAttribute. - Step 4: Run — PASS (add an absent-attribute case + a not-found case returning the input unchanged).
- Step 5:
StudioInspector.DrawProperties: whenKind==Markup, render rect (Left/Top/Width/Height) + anchors as ImGuiInputInt/combo; on change, set the liveUiElementfield AND call back to the window toMarkupWriteBack.SetAttribute(...)+ write the file (the watcher then reloads, or skip the watcher for self-writes via a guard flag). - Step 6: Build green, VISUAL GATE: select an element in a markup panel, change its Width in Properties → the panel updates AND the file on disk shows the new value.
- Step 7: Commit.
Gate (visual): edit a rect in the inspector → live panel updates + the markup file is rewritten.
Commit: feat(studio): editable markup props with in-place write-back
Task 8: Render-config sliders (doll camera)
Files:
- Modify:
src/AcDream.App/Rendering/DollCamera.cs(make Eye/Target/Fov instance-settable — currentlystatic readonly),StudioInspector.cs(sliders),PaperdollViewportRenderer.cs(expose itsDollCameraor accept overrides) - Read first:
DollCamera.cs(current static fields),PaperdollViewportRenderer.cs:35(private readonly DollCamera _camera = new();).
DollCamera change: convert the static readonly Eye/Target/Up/FovRadians to instance properties with the retail defaults as initializers (keep the decomp citations). This is a safe, behavior-preserving change (same defaults) — verify the existing DollCameraTests still pass (they read cam.View).
Inspector: a "Render config" ImGui window shown when a doll viewport is selected: SliderFloat3("eye", ...), SliderFloat("fov", ...), SliderFloat("heading", ...) bound to the PaperdollViewportRenderer's camera (expose it) + the doll's heading. A "Copy to clipboard" button emits the C# literals.
- Step 1: Make
DollCameraEye/Target/Up/FovRadians instance properties (defaults = current retail values). Build + runDollCameraTests— expect PASS (defaults unchanged). - Step 2: Expose the camera from
PaperdollViewportRenderer(apublic DollCamera Camera => _camera;). - Step 3:
StudioInspector: render the Render-config window with sliders mutatingrenderer.Camera.{Eye,FovRadians}+ the doll heading; a copy button. - Step 4: Build green, VISUAL GATE: select the doll, drag the eye/FOV sliders → the doll re-frames live; copy emits the literals.
- Step 5: Commit.
Gate (visual): dragging the camera sliders re-frames the doll in real time.
Commit: feat(studio): render-config sliders for the doll camera
Wrap-up (after Task 8)
- Update
claude-memory/project_d2b_retail_ui.md(or a newproject_ui_studio.md) with the studio's architecture + the launch command + DO-NOT-RETRY notes; add aMEMORY.mdpointer. - Update the roadmap: note the UI Studio tool shipped (it's tooling, not a milestone phase — a one-line ledger entry).
- Full suite green; final commit.
Self-review notes
- Spec coverage: both sources (Tasks 2 dat / 6 markup) ✓; full fixtures (Task 4 + 5) ✓; inspector read (3) + editable markup (7) + render sliders (8) ✓; hot-reload (6) ✓; live doll (5) ✓; RenderBootstrap (1) ✓; ImGui chrome (3) ✓.
- GL-untestable tasks (1,2-window,3,5,6-window,8) use visual gates by design — the studio is a visual tool; pure-logic units (LayoutSource infos, FixtureProvider, MarkupWriteBack) have unit tests.
- Type consistency:
RenderStack/ImportedLayout/StudioOptions/LayoutSource/ClientObjectTablemember names match the research reference;ResolveChromebody must be copied from GameWindow (flagged in Task 1 Step 1). - Risk: Task 1 builds NEW code (no GameWindow edit) → zero game-regression risk, the de-risk decision from the spec's open risk. Task 5's sample Setup id is the one unknown literal — Task 5 Step reads the test fixtures to find a real humanoid Setup before coding.