# Campaign LA — launcher / installer / updater + retail character-select **Status:** ACTIVE (started 2026-08-14) **Spec (approved):** `docs/superpowers/specs/2026-08-14-launcher-campaign-design.md` **Memory crib:** `claude-memory/project_launcher_direction.md` **Branch:** `claude/acdream-launcher-credentials-4d2f7c` (merge to main at coherent checkpoints) Campaign LA ships the alpha launcher (Avalonia, Windows + Linux): triple-duty launcher + installer + updater, ThwargLauncher-model profiles with full in-UI CRUD, plaintext credential file (user-decided), file-contract orchestration of `AcDream.App` and `AcDream.Headless`, plugins + login commands on both hosts, the headless character probe, and the retail character-select screen (no Create). All architectural decisions live in the spec — this plan sequences the work. ## Process (binding) - **Fable plans/sequences/integrates. Sonnet implements bounded slices. Opus reviews at every slice boundary, dual-lens:** (a) architectural — ownership, layering, dependency-guard integrity, seams; (b) retail fidelity vs `docs/research/named-retail/` wherever the slice touches retail behavior. Findings → fixes → narrow re-review. - Max 3–4 agents in parallel including children; subagents never spawn subagents; implementer prompts carry spec+plan paths, files-to-read, acceptance criteria, commit style. - `dotnet build` + `dotnet test` green before a slice is DONE; ≥1 commit per slice tagged `Campaign LA`; retail deviations add their `docs/architecture/retail-divergence-register.md` row in the same commit; no workarounds without explicit user approval. - Connected/visual gates are the ONLY stop-and-wait points; each gets an exact script under `docs/research/` and non-blocked slices keep moving. ## Slice map | Slice | Deliverable | Depends on | |---|---|---| | LA0 | `AcDream.Platform` extraction (`ApplicationPathSet`) + guard amendments | — | | LA1 | Launch contract: App `--session-config` + stdin credential; status.jsonl writer both hosts; roster plumbing | LA0 | | LA2 | Headless probe mode + `idle` policy | LA1 | | LA3 | `AcDream.Launcher.Core`: profile store CRUD, config composition, spawn/supervise, status reader | LA0 (LA1 contract shapes) | | LA4 | `AcDream.Launcher` Avalonia UI: CRUD views, per-char settings, sessions, probe action | LA3 | | LA5 | Plugin hosting: headless `IPluginHost` + capability flag; session-driven plugin set both hosts | LA1 | | LA6 | Login commands: parser-core extraction + execution on both hosts | LA1, LA5 | | LA7 | Character-select: Runtime selection state + wire (delete/restore/error) + no-selector flow | LA1 | | LA8 | Character-select authored retail screen (flat listbox — NO 3D preview, recon-corrected) | LA7 | | LA9 | Installer: first-run wizard (DAT locate/validate, bake w/ progress, SHA record) | LA3, LA4 | | LA10 | Updater: GitHub Releases manifest, download/verify/install/swap, self-update | LA3, LA4 | | LA11 | Closeout: connected-gate script, roadmap/CLAUDE.md/memory, program ledger | all | Parallelism guide: LA3/LA4 (launcher side) proceed alongside LA5–LA8 (client side) — different assemblies, no shared files. LA9/LA10 close the launcher side; LA11 closes the campaign. ## Linux posture (binding — user decision 2026-08-14) Everything the launcher does must WORK ON LINUX in this campaign, except GUI client launches: the Linux graphical client is Slice L, parked at L1, resuming later ("ok we will do it later"). Concretely: - **Linux-shipping in LA:** the Avalonia launcher UI, profile CRUD + 0600-permission file, installer (manual DAT picker — the auto-detect paths are Windows-only; `acdream-bake` is GL-free and runs on Linux), updater (staged swap; Linux can replace a running binary but keep the same staged-atomic flow), headless launches with plugins + login commands, and the character probe. - **Launcher UX on Linux:** the `gui` / `guiSelect` launch modes render disabled with an explicit "requires the Linux graphical client (Slice L)" note — never a silent failure. - **Per-slice enforcement:** every slice touching Launcher.Core, Headless, Runtime, Bake, or Platform runs its test projects on Linux (native Ubuntu or WSL, matching the K-slice practice) before the slice is DONE; LA4/LA9/LA10 additionally prove a real `linux-x64` self-contained publish. LA11's connected-gate script gets a Linux section: launcher on Ubuntu doing CRUD, probe, headless launch with plugin + login commands, first-run install with a manual DAT path, and an update swap. - When Slice L later ships, the launcher's Linux GUI modes light up with NO launcher changes (the session-config contract is host-agnostic) — that expectation is part of LA's design acceptance. ## LA0 — `AcDream.Platform` extraction New BCL-only project `src/AcDream.Platform/` holding `ApplicationPathSet` + `IApplicationPathEnvironment` (today `src/AcDream.Runtime/Platform/ApplicationPathSet.cs` — self-contained, no intra-Runtime dependencies; clean cut). Runtime/App/Headless reference it. Recon facts (2026-08-14): blast radius is the definition, six source files (`GraphicalHostPlatformServices.cs`, `GraphicalLegacyConfigurationMigrator.cs`, `App/Program.cs`, `GameWindow.cs:533`, `HeadlessPathSet.cs`, `HeadlessPlatformEnvironment.cs`; two more files are doc-comment-only), two test files (`ApplicationPathSetTests.cs` moves to a new `tests/AcDream.Platform.Tests/`; `GraphicalLegacyConfigurationMigratorTests.cs` fixtures), and the dependency guards — CORRECTED post-review (the original recon here asserted the wrong guard, the C4-closeout failure mode): the K0 Headless guard (`HeadlessAssemblyReferencesOnlyTheRuntimeProject`) asserts HEADLESS's own csproj reference list, which this move does not touch — it stays UNCHANGED; the guard that actually needs amending is Runtime's own `RuntimeDependencyBoundaryTests.RuntimeProjectDeclaresOnlyApprovedProjectDependencies` (Runtime gains the `AcDream.Platform` reference), amended with a cited comment in the same commit. Namespace stays `AcDream.Runtime.Platform`? NO — rename to `AcDream.Platform` and fix the eight usings (clean naming beats avoiding a mechanical edit). Register new projects in `AcDream.slnx`. **Acceptance:** build + full test suite green; guard test asserts the new exact reference set; launcher-side consumability proven by the LA3 project referencing only `AcDream.Platform`. ## LA1 — launch contract (client side) Three pieces, one slice, because they share the session-config/status seam: 1. **App `--session-config `:** parsed once in `Program.cs` into `RuntimeOptions` (code-structure rule 4); carries endpoint, account, optional character selector, `Plugins`, `LoginCommands`, `Content` (DatDirectory/PreparedAssetPath), status-file path, credential reference. Recon: `Program.cs` has NO subcommand dispatch today — args handling is one positional DAT-dir (`Program.cs:35`), so the flag is purely additive (preserve the positional arg). The live-credential seam is a single call site (`SessionPlayerComposition.cs:1128-1135` → `LiveSessionConnectOptions`); the config path populates the same `RuntimeOptions` fields from a different source. Env-var dev flow untouched. App gains the `StandardInput` credential read (mirroring `HeadlessCredentialResolver.ResolveStandardInput` — one line, immediately wrapped in an erasable secret, redacted `ToString`; today `RuntimeOptions.LivePass` is a bare string — the config path must not widen that exposure). 2. **Status stream both hosts:** per-session `status.jsonl` (path given in config; absent → no writer constructed, zero cost). Versioned event vocabulary (`"v":1`): `started`, `connected`, `characterList`, `enteredWorld`, `pluginLoaded`/`pluginFailed`, `disconnected`, `exited`. Recon: today's `HeadlessDiagnosticWriter` is a single shared-stdout JSONL sink with four kinds (lifecycle/failure/event/resources) and NO per-session file — the status writer is a second, separate sink, not a rework of the diagnostics writer. App has no structured writer today; it gets the same shared implementation (lands in Runtime so both hosts borrow it). 3. **Roster plumbing:** `CharacterList.Parsed` is consumed inside `LiveSessionController.StartCore` (`LiveSessionController.cs:612`) and never escapes — add a typed roster report on the lifecycle-host seam (`ILiveSessionLifecycleHost`) so hosts can emit the `characterList` status event and (later) the char-select screen can populate. No behavior change to selection itself in this slice. **Acceptance:** round-trip tests (config → `RuntimeOptions`; stdin credential; status events in order with exact shapes; roster surfaced); App/Headless/ Runtime suites green; redaction test proves the password never appears in status/diagnostics output. ## LA2 — headless probe mode + `idle` policy Recon facts: the probe's shape already exists as the `NoCharacters` early-exit (`LiveSessionController.cs:613-622` → `StopCore()` → 4-stage `SessionScope.DrainTeardown`, graceful, `_inWorld == false` so no pre-logoff flush) — but it fires only on selection FAILURE and maps to exit code 5 (`HeadlessProcessHost.RunOnUpdateThread:203-212` treats any non-`Connected` start as `ConnectionError`). 1. **Probe:** a `Probe` flag on the connect options short-circuits `StartCore` right after `GetCharacters` (before `TrySelectCharacter`): report roster, `StopCore()`, return a NEW `LiveSessionStartStatus.ProbeComplete`. `HeadlessProcessHost` maps it to exit code 0 with a final `characterList` + `exited(reason: "probe")` status pair. Config: `mode: "probe"` on the session descriptor relaxes the `JsonRequired` character selector + policy for probe sessions ONLY (loader keeps strict validation otherwise — recon: violations currently surface as raw `JsonException` → exit 3; probe relaxation must be shape-level in the loader, not attribute removal). 2. **`idle` policy:** new consumer `HeadlessBotPolicy` id — enter world, run plugins/login-commands (arrive in LA5/LA6), stay until stopped, clean SIGINT teardown (K4's graceful-logout path already proves the mechanism). **Acceptance:** probe test (fixture session → roster event → graceful teardown receipt → exit 0, no `EnterWorld` on the wire); loader tests for probe-shape relaxation + strict normal validation; idle-policy lifecycle test; suites green. Connected verification (user gate, LA11 batch): live probe against ACE twice in a row with no lingering session (spec §11.9). ## LA3 — `AcDream.Launcher.Core` New BCL-only project + `tests/AcDream.Launcher.Core.Tests/`. References `AcDream.Platform` ONLY. - Profile store: `launcher-profiles.json` (spec §5 schema) — load/save/ validate, full CRUD operations, roster merge (fold `characterList` events in, preserving per-character user settings), 0600 on Linux. - Session-config composition: profile + install records → the LA1 config shape (typed writer; probe shape included). Passwords excluded — stdin only. - Process orchestration: spawn App/Headless per launch mode, feed password to child stdin then close, supervise lifetime, tail `status.jsonl` (share-tolerant reads), surface typed session state. - SHA-256 utility (pak record + download verify — consumed by LA9/LA10). **Acceptance:** CRUD/round-trip/merge tests; composition tests (all three modes + probe); supervision tests against a fake child process (echo script); status-tail tests including partial-line handling; suites green. ## LA4 — `AcDream.Launcher` (Avalonia) New Avalonia project (Windows + Linux). MVVM over Launcher.Core; no game solution references beyond `AcDream.Platform` transitively. - Views: server list → accounts → characters tree; add/edit/remove dialogs for servers (name/host/port) and accounts (account + password entry); per-character settings editor (launch mode, plugin set, login commands); per-account "refresh characters" (probe); running-sessions status column. - Launch actions per mode (`gui` / `guiSelect` / `headless`); probe disabled while the launcher runs a session for that account. - First-run wizard shell + update prompt shell (bodies land in LA9/LA10). **Acceptance:** ViewModel tests in Launcher.Core.Tests patterns (VMs live in the Avalonia project but stay logic-thin; anything testable pushes down); build green on Windows; `linux-x64` publish compiles. Visual polish is gated at LA11 (user). ## LA5 — plugin hosting on both hosts Recon facts (2026-08-14): `PluginLoader`/`PluginDiscovery`/`PluginManifest` already live in `AcDream.Core` (Headless-reachable). App's single load loop (`App/Program.cs:110-121`) loads ALL discovered plugins from two roots (`AppContext.BaseDirectory/plugins` + `ApplicationPathSet.PluginsDirectory`, dup-id skip) — no allow-list exists on either host. `AppPluginHost` is a 26-line pass-through; three of four `IPluginHost` surfaces (`State` → `WorldGameState`, `Events` → `WorldEvents`, `Selection` → `SelectionState`) are backed by Core-owned types already; only `Ui` (`BufferedUiRegistry`) is genuinely App-only. Headless has zero plugin hosting today (confirmed). 1. Session-config `Plugins` allow-list filters the discovery result on BOTH hosts (absent list = load all, preserving today's dev behavior). 2. `HeadlessPluginHost : IPluginHost` in Headless over the same Core-owned `State`/`Events`/`Selection`; `Ui` is an explicit no-op behind a new capability flag on `IPluginHost` (e.g. `HasUi`) so plugins can detect headless. Contract documented in `Plugin.Abstractions`. 3. `pluginLoaded`/`pluginFailed` status events from both hosts' load loops. **Acceptance:** fixture plugin in Headless suite (load, capability flag, markup no-op, teardown via collectible ALC); allow-list filter tests both hosts; status events asserted; suites green. ## LA6 — login commands on both hosts Recon facts (2026-08-14): the command core is dependency-CLEAN — `ChatInputParser` (zero usings), `ChatCommandRouter` (BCL + `AcDream.Core.Chat`), `RetailClientCommandCatalog` (FrozenDictionary), `ChatVM` (Core.Chat/Combat + `System.Numerics` only), `ICommandBus` + the four command records (BCL-only). The block is assembly identity, not coupling. `ChatCommandRouter.Submit`'s two entanglements: a hard `ChatVM` parameter (uses only `ShowInterfaceText`/`ShowSystemMessage`/ `LastIncomingTellSender`/`LastOutgoingTellTarget`) and the `ICommandBus`, whose production implementation (`LiveSessionCommandRouter`, `App/Net/LiveSessionCommandRouter.cs`) is App-only and wraps wire-send delegates from the live session. GUI already has a login-command analog: `RetailUiAutomationScriptRunner` feeds `ChatCommandRouter.Submit` at `RetailUiRuntime.cs:523-527`. 1. **Extraction:** move parser/router/catalog + `ICommandBus` + the four command records (+ sibling tables they require) into Runtime (`AcDream.Runtime/Chat/...`); the router's `ChatVM` parameter becomes a narrow feedback interface defined beside it (exactly the four members used); `ChatVM` (stays in UI.Abstractions) implements it. GUI path stays bit-identical — same call sites (`ChatWindowController.cs:326`, `FloatingChatWindowController.cs:157`), same routing, CH-accepted behavior regression-checked by the existing chat suites. 2. **Headless dispatch:** a Runtime/Headless `ICommandBus` binding the same session send delegates (`SendTalk`/`SendTell`/`SendChannel`/ `SendTurbineChat`) + Runtime state that App's router binds — paralleling `LiveSessionCommandRouter`'s registrations, feedback lands in `RuntimeCommunicationState.AddText`. 3. **Execution:** both hosts run `LoginCommands` sequentially as-if-typed (default 500 ms inter-command delay, config-overridable) once entered-world; per-command failures → status stream, never abort. 4. K0 guard: if the code folds into Runtime, the single-reference assertion stands untouched; the forbidden-prefix closure tests keep passing. Any guard text change is deliberate and documented. **Acceptance:** extraction lands with zero GUI chat test regressions (UI.Abstractions + App chat suites bit-green); headless executes a login-command script against a fixture session with ordered wire sends; delay + failure-tolerance tests; suites green. ## LA7 — character-select: state + wire Recon facts (2026-08-14): retail's screen is `gmCharacterManagementUI` (`acclient.h:56545`) — flat listbox + Create/Enter/Delete/Restore buttons + dialog contexts. **No 3D preview exists on retail's select screen** (the `gmCG3DView`/`CreatureMode` viewport is chargen-only; the old "rotating pedestal" line in `retail-ui/05-panels.md` §13 is uncited and wrong). Our `CharacterList` parse already matches ACE's serializer exactly (two-array shape, status/deleted always zero from ACE) and the two-phase enter-world (0xF7C8 → 0xF7DF → 0xF657) is implemented. Missing wire: delete/restore/error. 1. **Wire messages** (`AcDream.Core.Net/Messages/`, retail citations in file docs per house style): `CharacterDelete` 0xF655 — outbound account String16L + **slot index** (`Proto_UI::SendDeleteCharacter @0x00546b30`; NOT guid), inbound opcode-only ack followed by a fresh CharacterList; `CharacterRestore` 0xF7D9 guid-only (ACE + holtburger consensus; the decomp's apparent extra strings are a decompiler artifact — spec §11.4), response 0xF643 (flag + guid + name + secondsDisabled); `CharacterError` 0xF659 parser (new — today NO character-stage server error can be surfaced). 2. **Runtime selection state** (J-owner pattern): roster with per-entry greyed/pending-delete state (`SecondsGreyedOut != 0` ⇒ pending; ACE sends a constant 1 during the grace window — treat as boolean, never a countdown), highlight, pending-delete dialog state, typed commands (highlight / enter / delete-request / delete-confirm / restore). Retail behavior oracles: `RebuildCharacterList@0x004ec3a0`, `SelectCharacter@0x004ec160`, `UpdateButtons@0x004ec240` (Delete↔Restore swap on greyed state), `EnterGame@0x004ed440`. 3. **No-selector flow:** a graphical session config without a character selector stops at selection state instead of auto-enter; the first-available fallback (`CharacterList.TrySelectFirstAvailable`, used at `LiveSessionController.cs:848-851`) remains ONLY for selector-carrying/headless sessions. Selection feeds the existing `EnterWorld` path unchanged. **Acceptance:** message round-trip tests against ACE's serializer shapes; selection-state tests (greyed transitions, delete→list-refresh, restore, error surfacing); no-selector stop + enter flow tests; suites green. ## LA8 — character-select: authored retail screen Scope: project LA7's state through the REAL retail screen. No 3D preview (recon-corrected; a preview would be an unapproved divergence). 1. **Layout resolution:** retail resolves the root via `UIMainFramework::CreateAndAddRootElement(0x10000005, 0x1000039a)` + `DBObj::GetDIDByEnum(..., 5)` — reuse OP8's ported GetDIDByEnum machinery (category 4 precedent) for enum-table 5; slice starts by dumping that table from installed DATs to pin the concrete DataID. Child ids: listbox `0x1000039d`, create `0x100003a0` (present, disabled — Create is a future campaign), enter `0x100003a2`, delete `0x1000039f`, restore `0x1000039e`. 2. **Dialogs:** delete-confirm, please-wait, entering-world, error — the retail dialog machinery from the OP8 WaitDialog work (`2a81e813` mapped WaitDialog class type 0x19) is the base. 3. **Open item resolved here:** whether retail draws a render-loop background scene behind the UI (pseudo-C proves only that the UI class owns no viewport) — settle via user recollection + the visual gate before polishing. **Acceptance:** authored screen builds from DAT assets; button-state matrix matches `UpdateButtons` oracle (incl. Delete↔Restore swap); enter/delete/restore/error flows drive LA7 state end-to-end; suites green. User visual gate at LA11 (screen look, dialog flows, delete + restore against local ACE). ## LA9 — installer (first-run) - DAT locate: auto-detect `%USERPROFILE%\Documents\Asheron's Call` and `C:\Turbine\Asheron's Call` + manual picker; validate the four DATs. - Bake: spawn `acdream-bake --dat-dir --out /pak/acdream.pak --threads N`. Recon: default `--out` is INSIDE the DAT dir — the launcher always passes `--out` explicitly. Progress: add `--progress-json` to `AcDream.Bake` (JSONL progress lines alongside the existing 5-second human text, which stays default) — scraping human text is fragile and we own the tool. Recon: the bake has NO whole-file SHA — after a successful bake the LAUNCHER computes and records SHA-256 + size + `BakeToolVersion` in its install record, and re-verifies on subsequent startups (fast corruption check trades a few seconds of hashing for never launching against a half-written pak). - Install record feeds LA3's session-config composition (DatDirectory/PreparedAssetPath). **Acceptance:** wizard flow tests over Launcher.Core (fake bake child emitting `--progress-json` lines); bake-tool progress flag tests in `tests/AcDream.Bake.Tests`; SHA record/verify tests; suites green. Connected gate (user): clean-profile first-run against real DATs. ## LA10 — updater - Manifest: GitHub Releases; `manifest.json` release asset — version, per-RID client zip URL + SHA-256 + size, minimum-launcher version. Launcher pins owner/repo in its config. - Client update: poll on launch (+ manual check), download to staging, SHA verify, unpack to `DataDirectory/app//`, atomic `current.json` pointer swap, refuse while any session runs, keep previous version for one-step rollback. - Launcher self-update: staged download + rename-dance on next start. - Session-config composition targets `app/current`'s binaries. **Acceptance:** manifest/download/verify/swap tests against a local HTTP fixture; rollback test; refusal-while-running test; self-update staging test; suites green. Connected gate (user): staged-manifest update swap end-to-end. ## LA11 — closeout - One connected-gate script `docs/research/2026-XX-XX-campaign-la-test-script.md` covering: all three launch modes vs local ACE, probe round-trip ×2 (no lingering session), char-select visual matrix + delete flow, login-commands + plugin behavior on both hosts, add-server/add-account purely in UI, clean-profile first-run wizard, staged update swap. - Roadmap shipped-table entry, CLAUDE.md Current-state flip, memory distill, ledger below completed, program closeout section. ## Review protocol Per slice: implementer commit(s) → Opus dual-lens review (architectural + retail-where-applicable) → fix round → narrow re-review of fixes → slice DONE in ledger. Reviews name blast radius explicitly (`claude-memory/feedback_blast_radius_single_lens.md`). Slices LA7/LA8 add the retail-fidelity lens against named-retail symbols cited in the slice body; LA6 adds CH-regression scrutiny; LA0 adds guard-integrity scrutiny. ## Ledger | Slice | Status | Commits | Review | Notes | |---|---|---|---|---| | LA0 | review PASS; fix round applied | `cb6502c8` + fixes | Opus PASS w/ 6 findings 2026-08-14; narrow re-review pending | Byte-identity proven; CI Linux lanes + arch doc + Platform self-guard + App explicit ref fixed; plan recon corrected | | LA1 | in flight (Sonnet) | | | pinned contract v1 + 5 optional fields | | LA2 | — | | | | | LA3 | in flight (Sonnet, isolated worktree) | | | pinned contract shared with LA1 | | LA4 | — | | | | | LA5 | — | | | | | LA6 | — | | | | | LA7 | — | | | | | LA8 | — | | | | | LA9 | — | | | | | LA10 | — | | | | | LA11 | — | | | |