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;
}