372 lines
20 KiB
Markdown
372 lines
20 KiB
Markdown
# 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; 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
|
||
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):** 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.
|