acdream/docs/plans/2026-08-14-launcher-campaign.md
Erik 7f24af1a37 docs: Campaign LA Linux posture — full launcher stack Linux-tested; GUI waits for Slice L
User decision 2026-08-14: everything the launcher does ships Linux-tested
in this campaign (launcher UI, install/update with manual DAT picker,
headless launches with plugins + login commands, probe, per-slice Linux
test runs, Linux connected-gate section at LA11). GUI client launches
stay Windows-only until Slice L resumes later; the launcher renders GUI
modes disabled on Linux with an explicit note, and the host-agnostic
session-config contract means Slice L lights them up with no launcher
changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 15:17:53 +02:00

23 KiB
Raw Blame History

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 34 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 LA5LA8 (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/ or stays keyed to the new assembly; GraphicalLegacyConfigurationMigratorTests.cs fixtures), and ONE dependency guard: tests/AcDream.Headless.Tests/HeadlessDependencyBoundaryTests.cs HeadlessAssemblyReferencesOnlyTheRuntimeProject asserts Headless references exactly [AcDream.Runtime.csproj] — amend to the exact new set in the same commit (deliberate, never silent). 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 <path>: 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-1135LiveSessionConnectOptions); 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-622StopCore() → 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 (StateWorldGameState, EventsWorldEvents, SelectionSelectionState) 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 <dats> --out <DataDirectory>/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/<version>/, 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
LA1
LA2
LA3
LA4
LA5
LA6
LA7
LA8
LA9
LA10
LA11