acdream/docs/superpowers/specs/2026-08-14-launcher-campaign-design.md
Erik afb4e82943 docs: Campaign LA design spec — launcher/installer/updater + char select
Approved brainstorm outcome for the alpha launcher campaign: Approach A
file-contract orchestrator (session config in, stdin credential, JSONL
status events out), full in-UI CRUD for servers/accounts/credentials,
headless character-list probe, retail character-select screen (no
Create), plugins + login commands on both hosts, first-run DAT
locate/bake install, GitHub Releases update feed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 14:53:08 +02:00

17 KiB
Raw Blame History

Campaign LA — Launcher / Installer / Updater + character-select screen (design spec)

Date: 2026-08-14 Status: APPROVED design, pre-plan Plan doc (next step): docs/plans/2026-08-14-launcher-campaign.md (to be written) Prior decisions consumed: claude-memory/project_launcher_direction.md (2026-08-09)

Campaign LA is distinct from Modern Runtime Slice L (Linux graphical, parked at L1) — "LA" is a campaign identifier in the N/V/P/A/CH/OP/FA series, not a slice.

1. Goal

One external product — the acdream launcher — that is simultaneously the installer, the updater, and the multi-server / multi-account / multi-character session launcher (ThwargLauncher UX model), on Windows and Linux. Plus the one client-side feature the launcher flow exposes as missing: the retail character-selection screen.

Distribution model: alpha users receive ONLY the launcher (per-RID self-contained single-file publish). The launcher fetches the client from the release feed, locates retail DATs, runs the pak bake, and launches sessions.

2. Decisions log

Pinned 2026-08-09 (NOT re-litigated here):

Decision Value
UI stack Avalonia (Windows + Linux day one)
Project split AcDream.Launcher (thin Avalonia) + AcDream.Launcher.Core (BCL-only)
Credentials Plaintext file, user-decided. 0600 on Linux; never in logs/crash bundles
UX reference ThwargLauncher (servers × accounts × character pre-select)
Launch contract Reuse Slice K1's strict portable config shape
Paths Consume Slice L0's ApplicationPathSet XDG/Windows contract

Decided this session (2026-08-14):

Question Decision
Headless launch purpose Run plugins (VirindiTank-style) + login commands; same on GUI. Launcher selects which plugins per character
Character-select screen Retail screen, no Create. 3D rotating preview + Enter World + Delete; Create Character deferred to its own campaign
Campaign scope Everything now — launch flows + first-run install + update pipeline in one campaign
Launcher ↔ client coupling Approach A: file-contract orchestrator (config in, status events out; no game-protocol code in the launcher)
Update feed GitHub Releases (manifest.json + per-RID zips as release assets)
Profile editing Full CRUD in the launcher UI — add/edit/remove servers, accounts, passwords, per-character settings. The JSON file is storage (hand-editable as a bonus), never the required interface
Character enumeration On-demand probe: launcher spawns the headless host in a probe mode (connect → CharacterList → status event → graceful disconnect BEFORE entering world → exit) and folds the roster into the profile store

Rejected: the launcher itself embedding Runtime/Core.Net to speak the game protocol — the character probe runs in the headless host via the normal launch contract, so the launcher stays protocol-free. The probe never enters the world; a graceful account-level disconnect at the character-list stage is the same dance every normal login performs, so the ACE stale-session landmine (hard-killed in-world sessions poisoning the account ~3 min) does not apply on the happy path. Deferred: live IPC fleet dashboard (the status-file format is its forward seam), Create Character, community server-list import.

3. Architecture — file-contract orchestrator

The launcher never speaks the game protocol and references nothing from the game solution except a new tiny platform assembly. Its contracts with the client are exactly three:

  1. Config in — a per-launch session config file (K1 shape, extended).
  2. Credential in — password piped to child stdin (K1 StandardInput provider).
  3. Status out — a per-session JSON-lines event file the launcher tails.

Character enumeration has two feeds, both flowing through the same status-stream vocabulary:

  1. Cache-from-observation — hosts report the account's CharacterList in the status stream on every login; the launcher folds it into its profile store.
  2. On-demand probe — a "refresh characters" action per account spawns the headless host with a probe-mode session config: connect, receive CharacterList, emit the status event, gracefully disconnect without entering the world, exit. The launcher folds the roster in exactly as in (1). The launcher refuses to probe an account it is itself currently running a session for; an externally-active session makes the probe fail gracefully (reported on the status stream, never an exception in the launcher).

A never-seen account can therefore be enumerated before its first real launch, or simply launched in guiSelect mode and picked in-client.

4. Components

  • AcDream.Launcher.Core (new, BCL-only): profile store (load/save/validate/merge-charlist), session-config composition, process spawn + supervision + stdin credential feed, status-event reader, install engine (DAT locate/validate, bake-tool invocation, SHA verify), update engine (manifest client, download, SHA verify, versioned install, pointer swap), self-update stager. Fully unit-testable.
  • AcDream.Launcher (new, Avalonia): MVVM shell over Launcher.Core. Server list → accounts → characters tree with full CRUD in the UI: add/edit/remove servers (name/host/port), add/edit/remove accounts (account name + password entry), per-character settings editor (launch mode / plugins / login commands), a per-account "refresh characters" probe action, session status column, first-run install wizard, update prompts. Hand-editing the JSON is never required for any flow.
  • AcDream.Platform (new, tiny, BCL-only): ApplicationPathSet + IApplicationPathEnvironment move here from src/AcDream.Runtime/Platform/ApplicationPathSet.cs. Runtime, App, Headless, Launcher.Core reference it. Dependency guards (K0 family) amended deliberately in the same commit.
  • AcDream.App: gains --session-config <path> CLI ingestion into RuntimeOptions (env-var dev workflow untouched), the StandardInput credential resolver, the status-event writer, the launcher-selected plugin set, login-command execution, and the character-select screen.
  • AcDream.Headless: gains the two config fields (Plugins, LoginCommands), an idle consumer policy (enter world, run plugins/commands, stay until stopped), the probe mode (§3/§6), plugin hosting, and the same status-event writer.

5. Profile & credential store

One JSON file, created and maintained entirely by the launcher UI (the CRUD flows in §4): ConfigDirectory/launcher-profiles.json (%APPDATA%\acdream\ / ~/.config/acdream/), permissions 0600 on Linux. Hand-editability is a property of the format, not a required workflow.

{
  "version": 1,
  "servers": [
    {
      "name": "Local ACE",
      "host": "127.0.0.1",
      "port": 9000,
      "accounts": [
        {
          "account": "testaccount",
          "password": "testpassword",
          "characters": [
            {
              "name": "+Acdream",
              "id": "0x5000000A",
              "launchMode": "gui",
              "plugins": ["ExamplePlugin"],
              "loginCommands": ["/tell someone, hi", "/vt start"]
            }
          ]
        }
      ]
    }
  ]
}
  • characters[] = launcher-maintained cache (name/id, fed by status events) + user settings (launchMode, plugins, loginCommands).
  • launchMode: gui (straight to world), guiSelect (GUI, stop at character-select screen; default when no character chosen), headless.
  • Passwords live in this file and NOWHERE else: never in process arguments, never in session configs, never in logs (K1 redaction discipline extends to the launcher).
  • Server entries are manual-add (name/host/port). No published-list import this campaign.

6. Launch contract

Session config (written to CacheDirectory/launcher/sessions/<id>/session.json): the K1 HeadlessConfiguration shape extended with:

  • Plugins: string[] — plugin names to load from the standard PluginsDirectory; hosts load exactly this set.
  • LoginCommands: string[] — ordered chat-typed strings.
  • Graphical host: Character selector may be ABSENT → character-select screen instead of auto-enter.
  • Content descriptor (existing K1 field): DatDirectory + PreparedAssetPath, filled from the launcher's install records.

Spawn:

  • Headless: AcDream.Headless --config <path> (existing CLI).
  • GUI: AcDream.App --session-config <path> (new; parsed once in Program.cs into RuntimeOptions per code-structure rule 4).
  • Probe: AcDream.Headless --config <path> with a probe-mode session (connect → characterList status event → graceful disconnect before EnterWorld → exit). Today's config loader requires a character selector and a policy per session (JsonRequired); probe mode relaxes that for the probe shape only.
  • Credential: K1 StandardInput provider; launcher writes the password to child stdin then closes it. Headless supports this today; App gains the resolver.

Status stream (CacheDirectory/launcher/sessions/<id>/status.jsonl), appended by both hosts, one JSON object per line:

started, connected, characterList (names + ids + slots), enteredWorld (id + name), pluginLoaded / pluginFailed (name + error), disconnected, exited (code + reason).

The launcher tails this for live per-session UI state and folds characterList into the profile store. This exact event vocabulary is the seam a future IPC channel (fleet dashboard) replaces — same events, different transport — so event names/payloads are versioned from day one ("v": 1 per line).

7. Character-select screen (client-side, retail)

A new pre-world session state between CharacterList receipt and EnterWorld. Today LiveSessionController (TrySelectCharacter, src/AcDream.Runtime/Session/LiveSessionController.cs) auto-selects and enters immediately. New behavior: no character selector in options → stop at the retail character-select screen. Selection there feeds the same EnterWorld path.

  • Ownership: J-owner pattern. A Runtime-owned selection state (roster, highlighted entry, pending-delete confirmation) with typed commands (highlight / enter / delete-request / delete-confirm); App projects the authored screen. Headless never uses it (config always carries a selector; the loader already requires one).
  • UI: imported retail screen via LayoutImporter (OP3/FA recipe). The concrete LayoutDesc id and widget tree come from the grep-named-first workflow + docs/research/retail-ui/ during the plan; retail decomp is the behavior oracle for list interaction, Enter World, and Delete (including retail's delete confirmation flow — ACE serves the character-delete message).
  • 3D preview: a void-scene render path — the selected character's Setup + ObjDesc appearance rendered with a dedicated camera/lighting, reusing the existing creature-appearance pipeline outside a landblock. Own slice; the retail screen's rotation behavior is the oracle.
  • Non-goals: Create Character (own future campaign); a login screen ("back" exits the client — credentials always arrive via config/env).
  • Retail-workflow rules apply: any behavioral deviation ships with its divergence-register row in the same commit.

8. Plugins & login commands on both hosts

  • Plugin loading mechanics already live in Core (src/AcDream.Core/Plugins/PluginLoader.cs, collectible ALC, IPluginHost from AcDream.Plugin.Abstractions). This campaign makes the loaded SET session-config-driven on both hosts.
  • Headless plugin host: an IPluginHost implementation over Runtime state. UI-only surfaces (IUiRegistry.AddMarkupPanel, etc.) become explicit no-ops behind a capability flag so plugins can detect headless. Contract documented in Plugin.Abstractions.
  • Login commands run as if typed into chat: sequentially, with a default 500 ms inter-command delay (config-overridable per session), starting at entered-world. Failures are logged to the status stream and do not abort the session.
  • The parser seam: chat-string parsing/dispatch (ChatInputParser, ChatCommandRouter, 152-verb RetailClientCommandCatalog) lives in AcDream.UI.Abstractions, unreachable from Headless under the K0 dependency guard. The campaign extracts the parsing/dispatch CORE to a location both hosts reach — Runtime vs a small shared assembly is decided in the plan after reading the CH seams. The K0 guard amendment is a deliberate, documented change in the same slice. GUI chat behavior must be bit-identical before/after the extraction (CH campaign is closed and user-accepted; this must not reopen it).

9. Installer / updater

  • First-run wizard: auto-detect DAT directories (%USERPROFILE%\Documents\Asheron's Call, C:\Turbine\Asheron's Call)
    • manual picker; validate the four DAT files; run bake tool 4 with a real progress UI (~30 GB read); SHA-verify the pak; record DatDirectory + PreparedAssetPath for session configs.
  • Client install/update: poll the GitHub Releases feed's manifest.json (version, per-RID zip URL, SHA-256); download; verify; install to DataDirectory/app/<version>/; atomic pointer swap (current.json); never while any session is running; keep the previous version for one-step rollback.
  • Launcher self-update: same feed; staged download; rename-dance swap on next start (a running exe can't replace itself on Windows).
  • Feed hosting: GitHub Releases (user-confirmed). Manifest and zips are release assets; the launcher pins the repo/owner in its config.

10. Testing

  • Launcher.Core unit tests (new test project, registered in AcDream.slnx): profile round-trip + merge, full CRUD operations (add/edit/remove servers/accounts/characters surviving save/load), session-config composition (including the probe shape), manifest/SHA against a local HTTP fixture, process supervision + stdin feed against a fake child, self-update staging.
  • Launch contract: App/Headless suite round-trips — --session-configRuntimeOptions, stdin credential resolution, status-event writer output shape, plugin-set narrowing, login-command execution order.
  • Character-select: Runtime selection-state tests; authored screen via UI Studio dumps/screenshots; behavior against retail oracle.
  • Headless plugin host: fixture plugin in the Headless suite (load, capability flag, teardown).
  • Connected gates (user-driven): every launch mode against local ACE (gui / guiSelect / headless), the character probe (fresh account → refresh → roster appears, and repeated probes leaving no stale ACE session), clean-profile first-run wizard end-to-end, staged-manifest update swap, character-select visual matrix, delete flow, login-commands + plugin observable behavior on both hosts, and add-server/add-account flows done purely through the UI.

11. Risks / open items for the plan phase

  1. Parser-extraction landing spot (Runtime vs shared assembly) — decide after reading CH seams; do not regress CH-accepted chat behavior.
  2. Current App plugin-loading behavior (what loads today, when) — read before wiring the session-driven set.
  3. Retail character-select LayoutDesc id + widget tree — research task (grep-named + docs/research/retail-ui/).
  4. Character-delete wire message + ACE handling — verify against holtburger/ACE before implementing the Delete flow.
  5. Void-scene preview: lighting/camera parameters need the retail oracle (what does retail actually render behind the character?).
  6. Bake tool 4 invocation surface from Launcher.Core (in-process reference vs child process) — child process preferred to keep Launcher.Core free of game-solution references; confirm the tool's CLI is sufficient.
  7. AcDream.Platform extraction touches K0-family dependency guards — amend the guard assertions in the same commit, never loosen silently.
  8. Windows profile-file permissions: 0600 is Linux hygiene; Windows keeps default user-profile ACLs (no extra hardening — accepted plaintext posture).
  9. Probe semantics: verify against ACE source (and one live check) that a graceful disconnect at the character-list stage leaves no lingering account session — the design assumes the landmine is exclusive to hard-killed in-world sessions. Also verify ACE's behavior when a probe hits an account with an externally-active session (reject vs boot), and make the probe's failure path graceful either way.
  10. Probe config shape: the headless loader's JsonRequired character selector + policy need a deliberate relaxation for probe sessions only — normal sessions keep strict validation.