213 lines
8.3 KiB
C#
213 lines
8.3 KiB
C#
using System.Text.Json.Serialization;
|
|
|
|
namespace AcDream.Headless.Configuration;
|
|
|
|
internal sealed class HeadlessConfiguration
|
|
{
|
|
[JsonRequired]
|
|
public int Version { get; init; }
|
|
|
|
public HeadlessProcessSettings Process { get; init; } = new();
|
|
|
|
[JsonRequired]
|
|
public List<HeadlessSessionDescriptor?> Sessions { get; init; } = [];
|
|
}
|
|
|
|
internal sealed class HeadlessProcessSettings
|
|
{
|
|
public HeadlessPathOverrides Paths { get; init; } = new();
|
|
|
|
public HeadlessContentDescriptor? Content { get; init; }
|
|
}
|
|
|
|
internal sealed class HeadlessContentDescriptor
|
|
{
|
|
[JsonRequired]
|
|
public string DatDirectory { get; init; } = string.Empty;
|
|
|
|
[JsonRequired]
|
|
public string PreparedAssetPath { get; init; } = string.Empty;
|
|
|
|
public string? PreparedAssetOverlayPath { get; init; }
|
|
|
|
public uint? PreparedAssetBaseRecipeVersion { get; init; }
|
|
|
|
public uint? PreparedAssetEffectiveRecipeVersion { get; init; }
|
|
}
|
|
|
|
// MF-1 (Campaign OP OP7 review fix, 2026-08-11): record, not class — the
|
|
// direct-CLI launch path (HeadlessProcessHost.WithAccount) needs a `with`
|
|
// expression so adding a future property can't silently drop it from a
|
|
// hand-copied clone the way `CharacterOptions` was dropped here (the K3
|
|
// direct-credential launch mode reached HeadlessSessionHost with
|
|
// CharacterOptions == null, silently no-op'ing the whole OP7 feature).
|
|
internal sealed record HeadlessSessionDescriptor
|
|
{
|
|
[JsonRequired]
|
|
public string Id { get; init; } = string.Empty;
|
|
|
|
[JsonRequired]
|
|
public HeadlessEndpointDescriptor Endpoint { get; init; } = new();
|
|
|
|
[JsonRequired]
|
|
public string Account { get; init; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Campaign LA slice LA2: the JSON field is ABSENT for normal play
|
|
/// sessions (explicit JSON <c>null</c> is invalid);
|
|
/// <see cref="HeadlessSessionMode.Probe"/> for the LA2 probe
|
|
/// (connect → characterList → graceful disconnect, never EnterWorld) —
|
|
/// the pinned launch-contract schema's <c>mode</c> field
|
|
/// (<c>docs/plans/2026-08-14-launcher-campaign.md</c> LA1/LA2).
|
|
/// <see cref="Character"/>/<see cref="Policy"/> requiredness depends on
|
|
/// this value, which is why their requiredness lives in
|
|
/// <see cref="HeadlessConfigurationLoader"/>'s semantic validation rather
|
|
/// than a <c>[JsonRequired]</c> attribute — that attribute fires during
|
|
/// deserialization, before <see cref="Mode"/> can be inspected at all.
|
|
/// </summary>
|
|
public HeadlessSessionMode? Mode { get; init; }
|
|
|
|
/// <summary>
|
|
/// Required for play sessions (<see cref="Mode"/> absent); MUST be
|
|
/// omitted for probe sessions (<see cref="HeadlessSessionMode.Probe"/>) —
|
|
/// the pinned contract keeps the shape unambiguous by forbidding a probe
|
|
/// session from also declaring a selector. Enforced by
|
|
/// <see cref="HeadlessConfigurationLoader.ValidateSession"/>, not
|
|
/// <c>[JsonRequired]</c> (see this record's own doc on <see cref="Mode"/>).
|
|
/// </summary>
|
|
public HeadlessCharacterSelector? Character { get; init; }
|
|
|
|
/// <summary>Same mode-dependent requiredness as <see cref="Character"/>:
|
|
/// required for play sessions, forbidden for probe sessions.</summary>
|
|
public HeadlessBotPolicyDescriptor? Policy { get; init; }
|
|
|
|
[JsonRequired]
|
|
public HeadlessCredentialReference Credential { get; init; } = new();
|
|
|
|
/// <summary>
|
|
/// Campaign OP slice OP7 (2026-08-11), D8: optional declared character-
|
|
/// option overrides. Keys MUST be exact <c>CharacterOptionId</c> enum-
|
|
/// member spellings drawn from the lane-B tier-1+tier-2 bot-relevant
|
|
/// subset (docs/research/2026-08-10-character-options-map.md §5.2/§7.3)
|
|
/// — <see cref="HeadlessConfigurationLoader"/> rejects everything else
|
|
/// at load, before any name can reach the wire (ACE throws
|
|
/// <c>KeyNotFoundException</c> server-side on an unmodelled id — set-
|
|
/// character-options-wire.md §5.4.2). Dictionary VALUES bypass the
|
|
/// loader's camelCase property-naming policy entirely (only C# property
|
|
/// names go through that policy; JSON object keys inside a
|
|
/// <c>Dictionary<string, TValue></c> are read verbatim), so the
|
|
/// exact PascalCase enum spelling is what the config file must contain.
|
|
/// <c>null</c> (the field entirely absent) and an empty object are both
|
|
/// legal no-ops.
|
|
/// </summary>
|
|
public Dictionary<string, bool>? CharacterOptions { get; init; }
|
|
|
|
/// <summary>
|
|
/// Campaign LA slice LA1: plugin ids to load from the standard plugins
|
|
/// directory (<c>docs/plans/2026-08-14-launcher-campaign.md</c> LA1).
|
|
/// Absent means load every discovered plugin (the developer flow);
|
|
/// explicit empty means load none. LA5 host composition consumes this
|
|
/// as the actual allow-list filter.
|
|
/// </summary>
|
|
public List<string>? Plugins { get; init; }
|
|
|
|
/// <summary>
|
|
/// Campaign LA slice LA1/LA6: ordered chat-typed strings run through the
|
|
/// shared Runtime parser/router once the session enters world.
|
|
/// </summary>
|
|
public List<string>? LoginCommands { get; init; }
|
|
|
|
/// <summary>
|
|
/// Campaign LA slice LA1: inter-command delay for
|
|
/// <see cref="LoginCommands"/>, in milliseconds. Matches the pinned
|
|
/// launch-contract default (500 ms) when the field is absent from the
|
|
/// document.
|
|
/// </summary>
|
|
public int LoginCommandDelayMs { get; init; } = 500;
|
|
|
|
/// <summary>
|
|
/// Campaign LA slice LA1: absolute path for this session's status-event
|
|
/// JSONL stream (<c>docs/superpowers/specs/2026-08-14-launcher-campaign-design.md</c>
|
|
/// §6). Absent selects the writer's permanent no-op mode.
|
|
/// </summary>
|
|
public string? StatusFile { get; init; }
|
|
}
|
|
|
|
internal sealed class HeadlessEndpointDescriptor
|
|
{
|
|
[JsonRequired]
|
|
public string Host { get; init; } = string.Empty;
|
|
|
|
[JsonRequired]
|
|
public int Port { get; init; }
|
|
}
|
|
|
|
internal sealed class HeadlessCharacterSelector
|
|
{
|
|
public int? Index { get; init; }
|
|
public uint? Id { get; init; }
|
|
public string? Name { get; init; }
|
|
}
|
|
|
|
internal sealed class HeadlessBotPolicyDescriptor
|
|
{
|
|
[JsonRequired]
|
|
public string Id { get; init; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// Campaign FA slice FA6: optional role discriminator for a policy that
|
|
/// coordinates two bots run from the SAME process config (e.g. the
|
|
/// fellowship/allegiance gate's Leader — fellowship leader AND
|
|
/// allegiance patron — vs Recruit — fellowship recruit AND allegiance
|
|
/// vassal, docs/research/2026-08-11-fa-acdream-seams.md §6.2). Ignored
|
|
/// by every policy id that doesn't need it (all five pre-FA6 policies);
|
|
/// <see cref="AcDream.Headless.Policies.HeadlessBotPolicyFactory.Create"/>
|
|
/// rejects a missing role for a policy id that requires one.
|
|
/// </summary>
|
|
public HeadlessBotPolicyRole? Role { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign LA slice LA2: see <see cref="HeadlessSessionDescriptor.Mode"/>.
|
|
/// The pinned launch-contract schema defines exactly two states for a
|
|
/// session — ABSENT (mapped to <see langword="null"/>, meaning "play") or
|
|
/// the literal string <c>"probe"</c> — so <see cref="Probe"/> is the only
|
|
/// member; there is no explicit "play" spelling. This deliberately uses
|
|
/// <see cref="HeadlessConfigurationLoader"/>'s global camel-case,
|
|
/// string-only enum converter; a per-enum converter with its default options
|
|
/// would accidentally accept numeric <c>0</c> as a second probe spelling.
|
|
/// </summary>
|
|
internal enum HeadlessSessionMode
|
|
{
|
|
Probe,
|
|
}
|
|
|
|
/// <summary>See <see cref="HeadlessBotPolicyDescriptor.Role"/>.</summary>
|
|
[JsonConverter(typeof(JsonStringEnumConverter<HeadlessBotPolicyRole>))]
|
|
internal enum HeadlessBotPolicyRole
|
|
{
|
|
/// <summary>Fellowship leader / allegiance patron — creates the
|
|
/// fellowship, recruits the Recruit bot, and receives its oath.</summary>
|
|
Leader,
|
|
|
|
/// <summary>Fellowship recruit / allegiance vassal — gets recruited and
|
|
/// swears to the Leader bot.</summary>
|
|
Recruit,
|
|
}
|
|
|
|
[JsonConverter(typeof(JsonStringEnumConverter<HeadlessCredentialProviderKind>))]
|
|
internal enum HeadlessCredentialProviderKind
|
|
{
|
|
Environment,
|
|
StandardInput,
|
|
File,
|
|
}
|
|
|
|
internal sealed class HeadlessCredentialReference
|
|
{
|
|
[JsonRequired]
|
|
public HeadlessCredentialProviderKind Provider { get; init; }
|
|
|
|
[JsonRequired]
|
|
public string Reference { get; init; } = string.Empty;
|
|
}
|