diff --git a/docs/superpowers/specs/2026-08-14-launcher-campaign-design.md b/docs/superpowers/specs/2026-08-14-launcher-campaign-design.md new file mode 100644 index 00000000..1003a54a --- /dev/null +++ b/docs/superpowers/specs/2026-08-14-launcher-campaign-design.md @@ -0,0 +1,326 @@ +# 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 ` 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. + +```json +{ + "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//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 ` (existing CLI). +- GUI: `AcDream.App --session-config ` (new; parsed once in + `Program.cs` into `RuntimeOptions` per code-structure rule 4). +- Probe: `AcDream.Headless --config ` 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//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//`; 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-config` + → `RuntimeOptions`, 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.