acdream/docs/superpowers/specs/2026-08-14-launcher-campaign-design.md

389 lines
21 KiB
Markdown
Raw Permalink 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.** CORRECTED by 2026-08-14 recon: retail's `gmCharacterManagementUI` is a flat listbox + Enter Game / Delete / Restore buttons + dialogs — **no 3D preview exists on retail's select screen** (that machinery, `gmCG3DView`, is character-CREATION-only). We port what retail actually had; Create 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 |
| Linux scope | **Full launcher stack Linux-tested in LA** (launcher UI, install/update, headless + plugins + probe); GUI launches stay Windows-only until Slice L resumes later (user 2026-08-14). Launcher disables GUI modes on Linux with an explicit note |
| 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`; absent/null loads all discovered plugins, while an
explicit empty array loads none. Launcher-composed normal-empty and probe
sessions emit the empty array.
- `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), `loginCommandFailed` (zero-based command index + exact configured
line + isolated 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.
**CORRECTED 2026-08-14 (named-retail recon):** retail's screen is
`gmCharacterManagementUI` (`acclient.h:56545`) — a `UIElement_ListBox`
character list plus Create / Enter Game / Delete / Restore buttons and
dialog contexts (delete-confirm, please-wait, entering-world, error).
**It has NO 3D preview** — the rotating-model viewport (`gmCG3DView` /
`UIElement_Viewport` / `CreatureMode`) exists only on character
CREATION's appearance/heritage/profession pages. The earlier
"3D preview on a pedestal" belief traced to one uncited line in
`docs/research/retail-ui/05-panels.md` §13. We port the real screen; a
preview would be a deliberate divergence we are NOT taking.
- **Ownership:** J-owner pattern. A Runtime-owned selection state (roster
incl. greyed/pending-delete seconds, highlighted entry, pending-delete
confirmation) with typed commands (highlight / enter / delete-request /
delete-confirm / restore); App projects the authored screen. Headless
never uses it (config always carries a selector; the loader already
requires one).
- **UI:** imported retail screen. The root layout id is resolved
indirectly — retail calls
`UIMainFramework::CreateAndAddRootElement(0x10000005, 0x1000039a)` and
resolves the concrete DataID via `DBObj::GetDIDByEnum(..., 5)` (the same
GetDIDByEnum machinery OP8 already ported for key names, category 4).
Child widget ids from the decomp: listbox `0x1000039d`, create
`0x100003a0` (hidden/no-op this campaign), enter `0x100003a2`, delete
`0x1000039f`, restore `0x1000039e`. Behavior oracles:
`RebuildCharacterList@0x004ec3a0`, `SelectCharacter@0x004ec160`,
`UpdateButtons@0x004ec240` (Delete↔Restore visibility swap on greyed
state), `EnterGame@0x004ed440`,
`MakeDeleteCharacterConfirmationDialog@0x004ecca0`.
- **Wire (new messages):** `CharacterDelete` 0xF655 (outbound: account
String16L + **slot index**, per `Proto_UI::SendDeleteCharacter@0x00546b30`
— NOT the guid; inbound: opcode-only ack, then a fresh CharacterList),
`CharacterRestore` 0xF7D9 (guid) with response 0xF643, and a
`CharacterError` 0xF659 parser (currently absent — acdream cannot
surface any character-stage server error today). Enter-world's
two-phase handshake (0xF7C8 → 0xF7DF → 0xF657) is already implemented.
- **Open items** carried to the plan: the concrete layout DataID (dump
enum-table 5 from installed DATs), and whether retail rendered any
render-loop-level background scene behind the UI (the pseudo-C only
proves the UI class owns no viewport) — both resolved in the screen
slice before the user visual gate.
- **Non-goals:** Create Character (own future campaign; the Create button
exists on the authored screen but is disabled); 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; target-local atomic
replacement on next start after the running process exits.
- **Feed hosting:** GitHub Releases (user-confirmed). Manifest and zips
are release assets; the launcher pins the repo/owner in its config.
The exact v1 manifest, extracted-version record, `current.json` activation
pointer and launcher ownership record, shared-session/exclusive-update OS
lease, and durable self-update plan schema 3 are pinned in
`docs/plans/2026-08-14-launcher-campaign.md` under **Pinned updater
contracts (v1, BINDING)**. That section is normative: implementations reject
unknown/duplicate fields and unsupported versions, use strict SemVer 2.0
precedence, verify bounded streamed downloads before safe ZIP extraction, and
use per-hop redirect validation plus same-filesystem atomic replacement. The
LA9 DAT/pak install record remains the sole content descriptor fed to session
configs; LA10 changes only which verified `app/current.json` client binaries
the process supervisor executes.
## 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
exercised by focused App tests (UI Studio no longer exists — deleted at
Campaign V); visuals settle at the user gate.
- **Headless plugin host:** fixture plugin in the Headless suite
(load, capability flag, teardown).
- **Connected gates (user-driven):** execute the exact serial matrix in
`docs/research/2026-08-14-campaign-la-test-script.md` only after its
connection-free automated preflight passes. The launcher uses one immutable
process-local config/data/cache path set for the whole matrix; the local feed
URI reaches only updater composition and is never persisted. Cover 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. RESOLVED by recon except the concrete root-layout DataID: widget tree +
behavior symbols are in §7; the DataID hides behind `GetDIDByEnum`
enum-table 5 (enum `0x10000005`) — dump the table from installed DATs
in the screen slice.
4. RESOLVED by recon, CORRECTED by the LA7a Opus review (2026-08-14):
delete = 0xF655 account+slot (ack opcode-only, then fresh
CharacterList); restore = 0xF7D9 → 0xF643 response; ACE's
`secondsSincePendingDelete` computes to a constant 1 during the grace
window (ACE quirk — treat any non-zero as "pending delete", don't
display it as a countdown). The restore "two extra strings" question
resolved AGAINST the earlier reading: the PDB-paired binary shows two
REAL constant-string arguments (the decompiler mangled their
rendering, not their existence), so retail's request is ≥16 bytes and
our guid-only 8-byte form is an ADAPTATION — register row AD-97,
filed with the LA7a fix round. ACE reads only the guid; holtburger
ships guid-only successfully.
5. Whether retail rendered a render-loop-level background scene behind the
character-management UI — pseudo-C only proves the UI class owns no
viewport. Resolve in the screen slice (user memory of retail + the
visual gate settle it).
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.