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>
This commit is contained in:
Erik 2026-08-14 14:53:08 +02:00
parent cca8f20738
commit afb4e82943

View file

@ -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 <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.