acdream/src/AcDream.App/Program.cs
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

81 lines
2.5 KiB
C#

using AcDream.App;
using AcDream.App.Plugins;
using AcDream.App.Rendering;
using AcDream.Core.Plugins;
using Serilog;
if (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;
}
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
var datDir = args.FirstOrDefault() ?? Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (string.IsNullOrWhiteSpace(datDir))
{
Log.Error("usage: AcDream.App <dat-directory> (or set ACDREAM_DAT_DIR)");
return 2;
}
// Single read of the startup-time process environment. Every downstream
// consumer (GameWindow + collaborators) reads the typed bundle, not the
// raw env vars. See docs/architecture/code-structure.md §2 Rule 4.
var runtimeOptions = RuntimeOptions.FromEnvironment(datDir);
var worldGameState = new AcDream.Core.Plugins.WorldGameState();
var worldEvents = new AcDream.Core.Plugins.WorldEvents();
var uiRegistry = new AcDream.App.Plugins.BufferedUiRegistry();
var host = new AppPluginHost(new SerilogAdapter(Log.Logger), worldGameState, worldEvents, uiRegistry);
var pluginsDir = Path.Combine(AppContext.BaseDirectory, "plugins");
Log.Information("scanning plugins in {PluginsDir}", pluginsDir);
var loaded = new List<LoadedPlugin>();
foreach (var result in PluginDiscovery.Scan(pluginsDir))
{
if (!result.Success)
{
Log.Warning("plugin discovery failed for {Dir}: {Error}", result.PluginDirectory, result.Error);
continue;
}
var loadResult = PluginLoader.Load(result.PluginDirectory, result.Manifest!, host);
if (!loadResult.Success)
{
Log.Warning("plugin load failed for {Id}: {Error}", result.Manifest!.Id, loadResult.Error);
continue;
}
loaded.Add(loadResult);
Log.Information("loaded plugin {Id} ({DisplayName})", result.Manifest!.Id, result.Manifest.DisplayName);
}
try
{
foreach (var plugin in loaded)
{
try { plugin.Plugin!.Enable(); }
catch (Exception ex) { Log.Error(ex, "plugin enable failed: {Id}", plugin.Manifest.Id); }
}
using var window = new GameWindow(runtimeOptions, worldGameState, worldEvents, uiRegistry);
window.Run();
}
finally
{
foreach (var plugin in loaded)
{
try { plugin.Plugin!.Disable(); }
catch (Exception ex) { Log.Error(ex, "plugin disable failed: {Id}", plugin.Manifest.Id); }
}
Log.CloseAndFlush();
}
return 0;