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

326 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
```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/<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-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.