using System.Text.Json.Serialization; namespace AcDream.App.Configuration; /// /// Campaign LA slice LA1: the graphical host's reader for the pinned /// session-config document shape shared with /// AcDream.Headless.Configuration.HeadlessConfiguration — see /// docs/plans/2026-08-14-launcher-campaign.md LA1 and /// docs/superpowers/specs/2026-08-14-launcher-campaign-design.md §6. /// /// /// This is a DELIBERATELY independent DTO set, not a shared type reused from /// AcDream.Headless — Headless's config types are internal, tied to /// its own OP7 characterOptions allow-list semantics, and Headless is /// not a project App references. The two readers are cross-checked instead /// by a shared fixture document both test suites parse /// (SessionConfigurationSharedFixtureTests / /// HeadlessConfigurationSharedFixtureTests). /// /// /// /// Differences from the Headless reader, all intentional per the pinned /// contract: is OPTIONAL here /// (absent = today's first-available fallback; the character-select screen /// is LA7, not this slice); is parsed /// but never consulted (App has no bot-policy concept); exactly ONE session /// is required, not "one or more". /// /// internal sealed class SessionConfiguration { [JsonRequired] public int Version { get; init; } public SessionProcessSettings? Process { get; init; } [JsonRequired] public List Sessions { get; init; } = []; } internal sealed class SessionProcessSettings { public SessionContentDescriptor? Content { get; init; } /// Campaign LA slice LA1 review fix (F2): accepted so the SAME /// document also satisfies the Headless loader's own /// process.paths member (HeadlessPathOverrides) — parsed /// and ignored here, exactly like /// and below. App has /// no config/data/cache directory override concept of its own (those /// come from ApplicationPathSet/env vars on this host); only the /// Headless host consumes overrides composed under this key. public SessionProcessPathOverrides? Paths { get; init; } } /// Accepted-but-ignored mirror of Headless's /// HeadlessPathOverrides shape — see /// . internal sealed class SessionProcessPathOverrides { public string? ConfigDirectory { get; init; } public string? DataDirectory { get; init; } public string? CacheDirectory { get; init; } } internal sealed class SessionContentDescriptor { [JsonRequired] public string DatDirectory { get; init; } = string.Empty; [JsonRequired] public string PreparedAssetPath { get; init; } = string.Empty; } internal sealed record SessionDescriptor { [JsonRequired] public string Id { get; init; } = string.Empty; [JsonRequired] public SessionEndpointDescriptor Endpoint { get; init; } = new(); [JsonRequired] public string Account { get; init; } = string.Empty; /// Optional for the graphical host: absent means today's /// existing first-available fallback stays in effect. The retail /// character-select screen (LA7) is what actually consumes "no /// selector" as "stop and let the user pick". public SessionCharacterSelectorDescriptor? Character { get; init; } /// Accepted so the SAME document also satisfies the Headless /// loader's JsonRequired policy field — parsed and ignored here; /// App has no bot-policy concept. public SessionPolicyDescriptor? Policy { get; init; } /// Campaign LA slice LA1 review fix (F2): pinned-contract /// mode discriminator. ABSENT means today's ONLY App behavior — an /// ordinary play session — so every document written before this field /// existed keeps parsing unchanged. "probe" (LA2's connect /// ▸ characterList ▸ graceful-disconnect flow, no EnterWorld) is /// HEADLESS-ONLY; the App loader rejects it with an explicit message /// naming the field rather than the caller ever seeing a raw unmapped- /// member . Any other value /// is a configuration error — the pinned contract defines no other /// mode literal, so a document is either silent about mode (play) or /// says "probe" exactly. public string? Mode { get; init; } [JsonRequired] public SessionCredentialDescriptor Credential { get; init; } = new(); /// Accepted-but-ignored by App; Headless's own loader owns the /// allow-list semantics for this field (OP7 D8). public Dictionary? CharacterOptions { get; init; } /// LA1/LA5: plugin ids to load. Absent = load all; explicit /// empty = load none. public List? Plugins { get; init; } /// LA1/LA6: ordered chat-typed strings run through the shared /// Runtime parser/router after entering world. public List? LoginCommands { get; init; } /// LA1: inter-command delay for , /// milliseconds. Matches the pinned contract default of 500 ms. public int LoginCommandDelayMs { get; init; } = 500; /// LA1: absolute path for the status-event JSONL stream. /// Absent = no writer constructed. public string? StatusFile { get; init; } } internal sealed class SessionEndpointDescriptor { [JsonRequired] public string Host { get; init; } = string.Empty; [JsonRequired] public int Port { get; init; } } internal sealed class SessionCharacterSelectorDescriptor { public int? Index { get; init; } public uint? Id { get; init; } public string? Name { get; init; } } /// Loose by design: App never inspects the policy's shape beyond /// "does this document parse" — Id/Role stay untyped strings so /// this DTO never has to track Headless's own policy-id/role vocabulary. internal sealed class SessionPolicyDescriptor { public string? Id { get; init; } public string? Role { get; init; } } [JsonConverter(typeof(JsonStringEnumConverter))] internal enum SessionCredentialProviderKind { Environment, StandardInput, File, } internal sealed class SessionCredentialDescriptor { [JsonRequired] public SessionCredentialProviderKind Provider { get; init; } [JsonRequired] public string Reference { get; init; } = string.Empty; }