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>
This commit is contained in:
Erik 2026-06-25 14:26:47 +02:00
parent 79ee3ffbe2
commit df6b5b3b39
5 changed files with 387 additions and 0 deletions

View file

@ -0,0 +1,60 @@
namespace AcDream.App.Studio;
/// <summary>
/// Parsed options for the acdream UI Studio standalone tool.
/// Constructed by <see cref="Parse"/> from the command-line tokens that follow
/// the <c>ui-studio</c> dispatch token.
/// </summary>
public sealed record StudioOptions(string DatDir, uint? LayoutId, string? MarkupPath)
{
/// <summary>
/// Parse studio options from the args that come AFTER the <c>ui-studio</c> token.
///
/// <para>Positional (first non-flag arg): dat directory. Falls back to
/// <c>ACDREAM_DAT_DIR</c> when omitted.</para>
/// <para><c>--layout 0xNNNN</c>: hex LayoutDesc dat id to preview.</para>
/// <para><c>--markup &lt;path&gt;</c>: path to a KSML markup file (Task 6, unsupported for now).</para>
/// <para>When neither <c>--layout</c> nor <c>--markup</c> is given the default layout
/// <c>0x2100006C</c> (vitals) is used.</para>
/// </summary>
public static StudioOptions Parse(string[] args)
{
string? datDir = null;
uint? layoutId = null;
string? markupPath = null;
for (int i = 0; i < args.Length; i++)
{
if (args[i] == "--layout" && i + 1 < args.Length)
{
var raw = args[++i];
// Accept 0xNNNN or plain hex.
if (raw.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
raw = raw[2..];
if (uint.TryParse(raw, System.Globalization.NumberStyles.HexNumber, null, out var id))
layoutId = id;
}
else if (args[i] == "--markup" && i + 1 < args.Length)
{
markupPath = args[++i];
}
else if (!args[i].StartsWith('-'))
{
datDir ??= args[i];
}
}
// Fall back to ACDREAM_DAT_DIR when no positional dat dir was given.
datDir ??= Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (string.IsNullOrWhiteSpace(datDir))
throw new InvalidOperationException(
"ui-studio: dat directory required — pass as first arg or set ACDREAM_DAT_DIR.");
// Default layout: vitals (0x2100006C).
if (layoutId is null && markupPath is null)
layoutId = 0x2100006Cu;
return new StudioOptions(datDir, layoutId, markupPath);
}
}