using AcDream.App; using AcDream.App.Configuration; using AcDream.App.Credentials; using AcDream.App.Plugins; using AcDream.App.Platform; using AcDream.App.Rendering; using AcDream.Platform; using Serilog; GraphicalHostPlatformServices graphicalPlatform = GraphicalHostPlatformServices.Resolve(); graphicalPlatform.ConfigureWindowBackend(); ApplicationPathSet applicationPaths = graphicalPlatform.Paths; IReadOnlyList migratedConfigurationFiles = GraphicalLegacyConfigurationMigrator.Migrate(applicationPaths); Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.Console() .CreateLogger(); foreach (string migratedConfigurationFile in migratedConfigurationFiles) { Log.Information( "migrated legacy graphical configuration to {Path}", migratedConfigurationFile); } Log.Information( "graphical platform {RuntimeIdentifier}; native closure: {NativeDependencies}", graphicalPlatform.RuntimeIdentifier, string.Join( ", ", graphicalPlatform.NativeDependencies.Select( dependency => $"{dependency.Feature}={dependency.PublishedFileName}"))); // Campaign LA slice LA1: --session-config is purely additive โ€” the // existing one positional dat-dir argument and every ACDREAM_* env var keep // working exactly as before when the flag is absent. See // docs/plans/2026-08-14-launcher-campaign.md LA1. // // Review fix F5 (LA1 review round): a trailing, valueless --session-config // (the flag typed as the LAST argument, nothing after it) must be a hard // error, never a silent fall-through to the env-var path โ€” a launcher that // mis-composed its argv would otherwise appear to work while quietly // ignoring the session-config contract entirely. string? sessionConfigFlagPath = SessionConfigArgumentParsing.ExtractFlagValue( args, "--session-config", out bool sessionConfigFlagPresent); if (sessionConfigFlagPath is null && sessionConfigFlagPresent) { Log.Error( "--session-config requires a value (a path to the session-config document)."); return 2; } string[] positionalArgs = SessionConfigArgumentParsing.WithoutFlagAndValue(args, "--session-config"); var datDirArg = positionalArgs.FirstOrDefault(); var envDatDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR"); // 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. RuntimeOptions runtimeOptions; if (sessionConfigFlagPath is not null) { SessionConfiguration sessionConfig; SessionDescriptor session; try { (sessionConfig, session) = SessionConfigurationLoader.Load(sessionConfigFlagPath); } catch (Exception error) when (error is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException or System.Text.Json.JsonException or SessionConfigurationException) { Log.Error("--session-config invalid: {Error}", error.Message); return 2; } string? resolvedDatDir = NullIfEmpty(sessionConfig.Process?.Content?.DatDirectory) ?? NullIfEmpty(datDirArg) ?? NullIfEmpty(envDatDir); if (resolvedDatDir is null) { Log.Error( "usage: AcDream.App (or set ACDREAM_DAT_DIR, " + "or supply process.content.datDirectory in --session-config)"); return 2; } AppCredentialSecret? secret = null; try { var resolver = new AppCredentialResolver( Console.In, applicationPaths.ConfigDirectory, graphicalPlatform.OperatingSystem == GraphicalHostOperatingSystem.Linux); secret = resolver.Resolve(session.Id, session.Credential); runtimeOptions = RuntimeOptions.FromSessionConfig( resolvedDatDir, Environment.GetEnvironmentVariable, sessionConfigFlagPath, sessionConfig, session, secret.Reveal()); } catch (AppCredentialException error) { Log.Error("--session-config credential unavailable: {Error}", error.Message); return 2; } finally { secret?.Dispose(); } // Env-var flow untouched when the flag is absent; when both are present // the flag wins โ€” this line makes that explicit rather than silent. Log.Information( "--session-config {Path} present; overriding ACDREAM_LIVE*/ACDREAM_TEST_* " + "env-var live-session settings", sessionConfigFlagPath); } else { var datDir = datDirArg ?? envDatDir; if (string.IsNullOrWhiteSpace(datDir)) { Log.Error("usage: AcDream.App (or set ACDREAM_DAT_DIR)"); return 2; } runtimeOptions = RuntimeOptions.FromEnvironment(datDir); } if (runtimeOptions.DevTools) { Log.Information( "ACDREAM_DEVTOOLS=1: the ImGui developer UI was removed at Campaign V " + "slice V11 along with the OpenGL backend it required; this flag now " + "only selects the optional Vulkan validation/debug-utils extensions."); } var worldGameState = new AcDream.Core.Plugins.WorldGameState(); var worldEvents = new AcDream.Core.Plugins.WorldEvents(); var uiRegistry = new AcDream.App.Plugins.BufferedUiRegistry(); using var window = new GameWindow( runtimeOptions, worldGameState, worldEvents, uiRegistry, graphicalPlatform); var host = new AppPluginHost( new SerilogAdapter(Log.Logger), worldGameState, worldEvents, window.Selection, uiRegistry); GraphicalPluginSession pluginSession = GraphicalPluginSession.Create( applicationPaths, runtimeOptions.Plugins, runtimeOptions.SessionId ?? "app", host, window.StatusWriter); window.StartPluginHosting(pluginSession); try { try { window.Run(); } catch (NotSupportedException error) { Log.Error("{GraphicalStartupFailure}", error.Message); return 4; } } finally { Log.CloseAndFlush(); } return 0; // Campaign LA slice LA1: --session-config value-presence helper. The // flag/positional-argument extraction itself lives in // AcDream.App.Configuration.SessionConfigArgumentParsing (review fix F5) so // its trailing-flag edge case is unit testable. static string? NullIfEmpty(string? value) => string.IsNullOrWhiteSpace(value) ? null : value;