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:
parent
cca8f20738
commit
afb4e82943
1 changed files with 326 additions and 0 deletions
326
docs/superpowers/specs/2026-08-14-launcher-campaign-design.md
Normal file
326
docs/superpowers/specs/2026-08-14-launcher-campaign-design.md
Normal 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.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue