docs: Campaign LA — pinned launch-contract schema COMMITTED into plan LA1
The LA3 Opus review process note was right: the contract both sides implement lived only in orchestrator prompts, which is exactly the drift mode the pin exists to prevent (and it produced the paths-key CRITICAL). The schema, field rules, probe-mode discriminator, and status vocabulary are now a binding plan section; amendments change this text first, implementations second. Ledger: LA3 fix round dispatched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
0bcc7ba3a3
commit
db9ad53c1c
38 changed files with 2397 additions and 40 deletions
|
|
@ -50,6 +50,32 @@ public sealed record LiveSessionStartResult(
|
|||
LiveSessionCharacterSelection? Selection = null,
|
||||
Exception? Error = null);
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA slice LA1: one roster entry as reported by
|
||||
/// <see cref="CharacterList.Parsed"/> — decoupled from the wire type so the
|
||||
/// lifecycle-host seam does not leak <c>AcDream.Core.Net.Messages</c> shapes
|
||||
/// into every consumer.
|
||||
/// </summary>
|
||||
public readonly record struct LiveSessionRosterEntry(
|
||||
uint Id,
|
||||
string Name,
|
||||
uint SecondsGreyedOut);
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA slice LA1: the account's active-character roster, reported to
|
||||
/// <see cref="ILiveSessionLifecycleHost.ReportRoster"/> right after
|
||||
/// <c>CharacterList</c> arrives and BEFORE selection — see
|
||||
/// <c>docs/plans/2026-08-14-launcher-campaign.md</c> LA1 item 2. Hosts forward
|
||||
/// this to their status stream (<c>characterList</c> event) and, later
|
||||
/// (LA7/LA8), to the character-select screen. Deleted characters are
|
||||
/// deliberately excluded — the same candidate set
|
||||
/// <see cref="CharacterList.TrySelectFirstAvailable"/> already uses.
|
||||
/// </summary>
|
||||
public sealed record LiveSessionRosterReport(
|
||||
string AccountName,
|
||||
int SlotCount,
|
||||
IReadOnlyList<LiveSessionRosterEntry> Entries);
|
||||
|
||||
/// <summary>
|
||||
/// Runtime boundary for the domain and presentation sinks attached to one
|
||||
/// exact <see cref="WorldSession"/> generation. The controller owns the
|
||||
|
|
@ -61,6 +87,10 @@ public interface ILiveSessionLifecycleHost
|
|||
void ResetSessionState(RuntimeGenerationToken retiringGeneration);
|
||||
void ReportConnecting(string host, int port, string user);
|
||||
void ReportConnected();
|
||||
/// <summary>Campaign LA slice LA1: reported once per successful
|
||||
/// <c>CharacterList</c> receipt, right before character selection. See
|
||||
/// <see cref="LiveSessionRosterReport"/>.</summary>
|
||||
void ReportRoster(LiveSessionRosterReport roster);
|
||||
void ApplySelectedCharacter(LiveSessionCharacterSelection selection);
|
||||
void ApplyEnteredWorld(LiveSessionCharacterSelection selection);
|
||||
void DetachSession(WorldSession session);
|
||||
|
|
@ -610,6 +640,13 @@ public sealed class LiveSessionController
|
|||
return new LiveSessionStartResult(LiveSessionStartStatus.Deferred);
|
||||
|
||||
CharacterList.Parsed? characters = _operations.GetCharacters(session);
|
||||
if (characters is not null)
|
||||
{
|
||||
host.ReportRoster(BuildRosterReport(characters));
|
||||
if (!IsCurrent(scope, generation))
|
||||
return new LiveSessionStartResult(LiveSessionStartStatus.Deferred);
|
||||
}
|
||||
|
||||
if (characters is null
|
||||
|| !TrySelectCharacter(
|
||||
characters,
|
||||
|
|
@ -838,6 +875,29 @@ public sealed class LiveSessionController
|
|||
private LiveSessionStartResult ConnectedResult()
|
||||
=> new(LiveSessionStartStatus.Connected, _activeSelection);
|
||||
|
||||
/// <summary>Campaign LA slice LA1: projects the wire-shaped
|
||||
/// <see cref="CharacterList.Parsed"/> into the decoupled
|
||||
/// <see cref="LiveSessionRosterReport"/>. Deleted characters are
|
||||
/// excluded, matching <see cref="CharacterList.TrySelectFirstAvailable"/>'s
|
||||
/// candidate set.</summary>
|
||||
private static LiveSessionRosterReport BuildRosterReport(
|
||||
CharacterList.Parsed characters)
|
||||
{
|
||||
var entries = new LiveSessionRosterEntry[characters.Characters.Count];
|
||||
for (int i = 0; i < entries.Length; i++)
|
||||
{
|
||||
CharacterList.Character character = characters.Characters[i];
|
||||
entries[i] = new LiveSessionRosterEntry(
|
||||
character.Id,
|
||||
character.Name,
|
||||
character.SecondsGreyedOut);
|
||||
}
|
||||
return new LiveSessionRosterReport(
|
||||
characters.AccountName,
|
||||
characters.SlotCount,
|
||||
entries);
|
||||
}
|
||||
|
||||
private static bool TrySelectCharacter(
|
||||
CharacterList.Parsed characters,
|
||||
LiveSessionCharacterSelector? selector,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue