CC2 review PASS (checksum term set confirmed against the CG_Pack accumulator; account-outside-body and GetPackSize=172 independently proven), fix rounde77ebf10(F1 latch scope + pin test, AD-100, ACE double-NameInUse note, creationFailed reason/name split, pointer fix, retail-discriminator citations), narrow re-review CLOSED, residual anchor fix95e95bb6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
860 lines
52 KiB
Markdown
860 lines
52 KiB
Markdown
# 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)
|
||
|
||
### Pinned launch-contract schema (v1, BINDING — committed per LA3 review)
|
||
|
||
This text is the single source of truth for the launcher↔host file
|
||
contract. Both host readers (LA1), the composer (LA3), and the probe
|
||
loader (LA2) implement EXACTLY this; any change is an amendment to THIS
|
||
section first, implementations second. The LA1+LA3 merge adds a
|
||
cross-assembly test feeding a composer-produced document to both host
|
||
loaders — that test is the seam's permanent enforcement.
|
||
|
||
Session-config document (System.Text.Json, camelCase,
|
||
`UnmappedMemberHandling.Disallow`, camelCase string enums):
|
||
|
||
```json
|
||
{
|
||
"version": 1,
|
||
"process": {
|
||
"content": { "datDirectory": "...", "preparedAssetPath": "..." }
|
||
},
|
||
"sessions": [{
|
||
"id": "sess-1",
|
||
"endpoint": { "host": "127.0.0.1", "port": 9000 },
|
||
"account": "testaccount",
|
||
"mode": "probe",
|
||
"character": { "id": 1342177290 },
|
||
"policy": { "id": "idle" },
|
||
"credential": { "provider": "standardInput", "reference": "session" },
|
||
"plugins": ["ExamplePlugin"],
|
||
"loginCommands": ["/vt start"],
|
||
"loginCommandDelayMs": 500,
|
||
"statusFile": ".../launcher/sessions/sess-1/status.jsonl"
|
||
}]
|
||
}
|
||
```
|
||
|
||
Field rules:
|
||
- `process.paths` is OMITTED unless a caller genuinely supplies overrides
|
||
(never an empty object — the App reader has no `paths` member and
|
||
strict parsing rejects unknown keys; LA3 review finding 1).
|
||
- `mode`: ABSENT for normal play sessions; `"probe"` for the LA2 probe
|
||
(connect → characterList → graceful disconnect, no EnterWorld). The
|
||
headless loader accepts the field starting at LA2.
|
||
- `character`: exactly ONE of index|id|name; OMITTED entirely (not null)
|
||
for guiSelect and for probe sessions.
|
||
- `policy`: `{ "id": "idle" }` for headless play sessions ONLY; omitted
|
||
for gui/guiSelect/probe.
|
||
- `credential`: always `{ "provider": "standardInput", "reference":
|
||
"session" }` for launcher-composed configs.
|
||
- `plugins`: absent/null means load all discovered plugins (preserving the
|
||
developer flow); explicit `[]` means load none. Launcher-composed
|
||
normal-empty and probe sessions emit `[]` so they cannot load arbitrary
|
||
machine-local plugins.
|
||
- `loginCommands`/`loginCommandDelayMs`/`statusFile`: optional,
|
||
omitted-when-unset (never null, never `[]` for empty). Absent
|
||
`loginCommandDelayMs` means 500.
|
||
|
||
Status stream (`statusFile`, one JSON object per line, writer flushes per
|
||
line, writer opens `FileShare.Read`, tailer opens
|
||
`Read/FileShare.ReadWrite|Delete`): events `started`, `connected`,
|
||
`characterList{accountName,slotCount,characters[{id,name,secondsGreyedOut}]}`,
|
||
`enteredWorld{characterId,characterName}`, `pluginLoaded{plugin}`,
|
||
`pluginFailed{plugin,error}`,
|
||
`loginCommandFailed{commandIndex,command,error}`,
|
||
`characterCreated{guid,name}`, `creationFailed{code,reason,name}`,
|
||
`disconnected{reason}`,
|
||
`exited{code,reason}` — every line carries `"v":1`, `"e"`, `"t"`
|
||
(ISO-8601 UTC), `"sessionId"`. `secondsGreyedOut` is a uint on BOTH
|
||
sides. Unknown `e` values must parse to a typed Unknown event, never
|
||
throw; a known `e` with a wrong payload shape should be distinguishable
|
||
from an unknown `e` (LA3 review finding 12).
|
||
|
||
**Campaign CC CC2 amendment (this section is the contract; the writer and
|
||
tailer below implement it, in that order):** `characterCreated{guid,name}`
|
||
fires on the Ok reply to a `CharacterCreate` (opcode `0xF656`) request —
|
||
`guid`/`name` come straight off the shared `0xF643`
|
||
`CharGenVerificationResponse` Ok identity payload
|
||
(`AcDream.Core.Net.Messages.CharGenVerificationResponse`), deliberately
|
||
named `guid`/`name` rather than `characterId`/`characterName` to mirror
|
||
that payload's own field names and to read distinctly from
|
||
`enteredWorld` — a freshly created character is logged straight in by
|
||
retail without a fresh `characterList` (see that type's doc comment), so
|
||
`characterCreated` can precede an `enteredWorld` for the same character
|
||
rather than replacing it. `creationFailed{code,reason,name}` fires on any
|
||
non-Ok reply: `code` is the raw wire `CharGenVerificationResponse.Code`
|
||
value, `reason` is that code's enum member name (e.g. `"NameInUse"`) so a
|
||
reader gets a stable readable reason without hard-coding the numeric
|
||
mapping itself, and `name` is the ATTEMPTED character name so a launcher
|
||
can render "the name Bob is taken". (CC2 review F4: the enum member
|
||
originally rode the `name` key, colliding in meaning with
|
||
`characterCreated.name`; renamed before any consumer shipped.)
|
||
|
||
`loginCommandFailed.commandIndex` is the zero-based index in the configured
|
||
`loginCommands` array. `command` is the exact configured line and `error` is
|
||
the isolated parser/router/handler failure. The event is observational: the
|
||
host continues with the next configured line and never converts the command
|
||
failure into a login, plugin, session, or process failure.
|
||
|
||
**Known LA1 status limitation:** the stream has no independent mid-play
|
||
wire-drop detector. If a transport becomes silent without raising through the
|
||
host's tick/teardown path, no immediate `disconnected` line can be promised;
|
||
the launcher must not treat the absence of that line as proof that the socket
|
||
is healthy. Explicit reconnect is ordered and observable — it emits
|
||
`disconnected{reason:"reconnect"}` before the replacement connection's second
|
||
`connected` — and normal stop/process teardown closes any still-open
|
||
connection before `exited`. A future transport-health signal may improve the
|
||
timing without changing this pinned event vocabulary.
|
||
|
||
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-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 → permanent no-op sink). Versioned event
|
||
vocabulary (`"v":1`): `started`, `connected`, `characterList`,
|
||
`enteredWorld`, `pluginLoaded`/`pluginFailed`,
|
||
`loginCommandFailed`, `characterCreated`/`creationFailed` (Campaign CC
|
||
CC2), `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/null list = load all, preserving today's dev behavior;
|
||
explicit `[]` = load none). Launcher-composed normal-empty and probe
|
||
sessions emit `[]`.
|
||
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.
|
||
|
||
LA7b hazards carried from the LA7a review (2026-08-14): ACE's restore
|
||
handler has a SILENT no-reply path (unknown guid → `return;`, no 0xF643,
|
||
no 0xF659) — selection state must never block awaiting a restore reply;
|
||
outbound routing is delete via retail's SendToLogon, restore via
|
||
SendToControl, ACE replies on UIQueue; `charError.NumErrors` (0x19) is an
|
||
enum-range sentinel and must never render as a user-facing message.
|
||
Register row AD-97 (guid-only restore request, an adaptation) rides the
|
||
LA7a branch.
|
||
|
||
**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 + target-local atomic replacement 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.
|
||
|
||
### Pinned updater contracts (v1, BINDING)
|
||
|
||
This section is the single source of truth for every LA10 feed and on-disk
|
||
shape. Readers use strict, case-sensitive `System.Text.Json` parsing, reject
|
||
unknown or duplicate properties, and reject unsupported schema versions
|
||
before doing network, extraction, or activation work.
|
||
|
||
The production feed is pinned to GitHub owner/repository
|
||
`eriknihlen/acdream`; the launcher reads
|
||
`https://github.com/eriknihlen/acdream/releases/latest/download/manifest.json`.
|
||
Tests use a separate internal fixture constructor that may admit loopback HTTP;
|
||
that allowance never propagates to the production feed. Production manifest
|
||
and artifact URIs use HTTPS. Automatic redirects are disabled and every
|
||
redirect hop is validated before it is requested; redirect loops, a chain over
|
||
five hops, and any HTTPS-to-HTTP downgrade are rejected. `manifest.json` is:
|
||
|
||
```json
|
||
{
|
||
"schemaVersion": 1,
|
||
"version": "1.2.3",
|
||
"minimumLauncherVersion": "1.1.0",
|
||
"clients": {
|
||
"win-x64": {
|
||
"url": "https://github.com/eriknihlen/acdream/releases/download/v1.2.3/acdream-client-win-x64.zip",
|
||
"sha256": "<64 hex characters>",
|
||
"size": 123
|
||
}
|
||
},
|
||
"launchers": {
|
||
"win-x64": {
|
||
"url": "https://github.com/eriknihlen/acdream/releases/download/v1.2.3/acdream-launcher-win-x64.zip",
|
||
"sha256": "<64 hex characters>",
|
||
"size": 123
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
`version` and `minimumLauncherVersion` are strict SemVer 2.0 strings. Build
|
||
metadata is ignored for precedence; numeric identifiers are compared without
|
||
fixed-width integer overflow. RID keys are exact lowercase portable RIDs.
|
||
Both dictionaries are required and the running RID must have a client and a
|
||
launcher row. Artifact sizes are positive and capped by the launcher's
|
||
download limit; SHA-256 is exactly 64 hex characters. ZIP URLs are absolute.
|
||
Client ZIPs have the two host executables at their root
|
||
(`AcDream.App[.exe]`, `acdream-headless[.exe]`); launcher ZIPs have
|
||
`acdream-launcher[.exe]` at their root. No implicit wrapper directory exists.
|
||
|
||
Every extracted client version has
|
||
`DataDirectory/app/<version>/install.json`:
|
||
|
||
```json
|
||
{
|
||
"schemaVersion": 1,
|
||
"version": "1.2.3",
|
||
"rid": "win-x64",
|
||
"archiveSha256": "<64 hex characters>",
|
||
"archiveSize": 123,
|
||
"files": [
|
||
{ "path": "AcDream.App.exe", "sha256": "<64 hex characters>", "size": 123, "unixMode": 0 }
|
||
]
|
||
}
|
||
```
|
||
|
||
Paths use `/`, are relative, normalized, unique under ordinal-ignore-case,
|
||
and sorted ordinally. `unixMode` contains only the portable permission bits
|
||
captured from the ZIP entry. Startup verifies every recorded regular file by
|
||
size/SHA, rejects unrecorded files/reparse points, and requires the two host
|
||
executables before admitting a version. Extraction uses a random sibling
|
||
directory under `DataDirectory/app/`; promotion to `<version>/` is one
|
||
same-volume directory rename.
|
||
|
||
`DataDirectory/app/current.json` is the only activation authority:
|
||
|
||
```json
|
||
{ "schemaVersion": 1, "currentVersion": "1.2.3", "previousVersion": "1.1.0" }
|
||
```
|
||
|
||
`previousVersion` is omitted for the first activation. Pointer writes are
|
||
write-through temporary-file + same-directory atomic rename. The last valid
|
||
pointer is also atomically preserved as `current.previous.json`; startup may
|
||
restore that exact backup only when `current.json` is missing/malformed and
|
||
the referenced version verifies. Orphan LA10 staging directories, download
|
||
archives, corrupt-version quarantine directories, and pointer temporaries are
|
||
transaction-owned by exact lowercase GUID names and are removed only under the
|
||
exclusive update lease; near-matching user names are preserved. A corrupt
|
||
installed version is never silently selected; the explicit one-step rollback
|
||
swaps the two verified pointer versions.
|
||
|
||
`DataDirectory/app/.update-session.lock` is the cross-process barrier. Each
|
||
supervised launcher activity holds a shared OS handle from before executable
|
||
resolution until terminal process observation; launcher disposal requests
|
||
child termination and does not release that handle until the child is actually
|
||
observed terminal. An update/rollback holds the
|
||
exclusive handle for its entire recovery/download/extract/promote/pointer
|
||
transaction. Failure to acquire the exclusive handle is an immediate refusal,
|
||
not a wait behind a running session. The open handle, not lock-file contents,
|
||
owns the lease and therefore releases after process death.
|
||
|
||
Launcher self-update staging lives at
|
||
`DataDirectory/launcher-update/transactions/<transactionId>/` and the sole
|
||
durable authority is `DataDirectory/launcher-update/pending.json` (schema 3):
|
||
|
||
```json
|
||
{
|
||
"schemaVersion": 3,
|
||
"transactionId": "0123456789abcdef0123456789abcdef",
|
||
"state": "staged",
|
||
"version": "1.2.3",
|
||
"rid": "win-x64",
|
||
"targetDirectory": "<absolute current launcher directory>",
|
||
"archiveSha256": "<64 hex characters>",
|
||
"archiveSize": 123,
|
||
"files": [
|
||
{ "path": "acdream-launcher.exe", "sha256": "<64 hex characters>", "size": 123, "unixMode": 0 }
|
||
],
|
||
"apply": null
|
||
}
|
||
```
|
||
|
||
Before mutation the verified staged launcher becomes the next-start helper and
|
||
waits for the initiating launcher PID without invoking a shell. It first copies
|
||
the complete verified payload into the target-local
|
||
`.acdream-self-update-<transactionId>/incoming/` tree. The plan then advances
|
||
to `applying`; `apply` is an ordinally sorted union of new payload paths, the
|
||
owned metadata path, and obsolete paths from the previous ownership record:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"path": "acdream-launcher.exe",
|
||
"operation": "install",
|
||
"hadOriginal": true,
|
||
"priorSha256": "<64 hex characters>",
|
||
"priorSize": 123,
|
||
"priorUnixMode": 0,
|
||
"replacementSha256": "<64 hex characters>",
|
||
"replacementSize": 456,
|
||
"replacementUnixMode": 0
|
||
},
|
||
{
|
||
"path": "new-support.dat",
|
||
"operation": "install",
|
||
"hadOriginal": false,
|
||
"priorSha256": null,
|
||
"priorSize": null,
|
||
"priorUnixMode": null,
|
||
"replacementSha256": "<64 hex characters>",
|
||
"replacementSize": 456,
|
||
"replacementUnixMode": 0
|
||
}
|
||
]
|
||
```
|
||
|
||
Every `hadOriginal` entry persists the exact pre-mutation SHA-256, length, and
|
||
Linux mode bits; a no-original entry has all three prior fields null. Every
|
||
install entry likewise persists the verified replacement metadata, while a
|
||
remove entry has all three replacement fields null. The journal is invalid
|
||
unless those fields agree with `hadOriginal` and `operation`.
|
||
|
||
Existing targets are replaced with one same-filesystem atomic replace whose
|
||
backup is also target-local. Previously absent noncanonical files use one
|
||
same-filesystem rename; obsolete owned files use one rename into backup. The
|
||
canonical launcher path therefore contains either the complete old file or the
|
||
complete new file at every durable crash boundary. Rollback first performs a
|
||
zero-mutation preflight of the complete target-local transaction and every
|
||
journal entry. It rejects reparse points, unsafe parents, unrecorded paths,
|
||
ambiguous file layouts, and any SHA-256/length/mode mismatch in a prior,
|
||
incoming, or discard file. Only a fully preflighted rollback may atomically
|
||
restore backups; newly created files move to target-local discard rather than
|
||
being deleted. The complete prior target set is then reverified before the
|
||
plan enters durable `rolledBack` state while retaining the journal. Retry is
|
||
allowed only after that prior set is reverified again and the plan returns to
|
||
`staged`. Thus rollback is atomic per file and idempotent after a process/power
|
||
loss. Any ambiguity preserves the applying plan and transaction evidence and
|
||
forbids launching the canonical path for manual recovery. Linux mode bits come
|
||
from the verified incoming file. A helper that cannot immediately
|
||
acquire the exclusive update lease defers the staged plan and exits without
|
||
restarting the old launcher, preventing restart loops.
|
||
|
||
Successful application writes strict target ownership metadata at
|
||
`<launcher directory>/launcher.install.json`:
|
||
|
||
```json
|
||
{
|
||
"schemaVersion": 1,
|
||
"version": "1.2.3",
|
||
"rid": "win-x64",
|
||
"files": [
|
||
{ "path": "acdream-launcher.exe", "sha256": "<64 hex characters>", "size": 123, "unixMode": 0 }
|
||
]
|
||
}
|
||
```
|
||
|
||
The archive may not supply that reserved metadata path. A prior valid record is
|
||
the only authority for obsolete-file removal; the first managed update does
|
||
not infer ownership of unrelated legacy files. On success the plan becomes
|
||
`awaitingConfirmation`; the new launcher confirms at its first managed
|
||
instruction, after which the helper releases its lease and the confirmed
|
||
launcher reclaims plan, data-transaction, and target-local residue. An
|
||
`applying` plan is rolled back before retry, and failure to start/confirm the
|
||
new launcher restores every original (and removes every no-original target).
|
||
The helper restarts the restored canonical launcher only after a fresh complete
|
||
verification of the retained `rolledBack` journal; rollback corruption or an
|
||
unsafe backup/discard tree exits without starting either launcher.
|
||
Reading `pending.json` never performs cleanup. Ordinary startup attempts the
|
||
exclusive lease without waiting and skips update cleanup entirely when another
|
||
session/staging transaction owns it. All plan paths are re-derived/contained
|
||
under pinned roots; the target directory must equal the actual launcher base
|
||
directory.
|
||
|
||
Every portable archive and persisted relative path rejects Windows device
|
||
segments on every host: `CON`, `PRN`, `AUX`, `NUL`, `CLOCK$`, `CONIN$`,
|
||
`CONOUT$`, `COM1`-`COM9`, `LPT1`-`LPT9`, and the Windows-equivalent superscript
|
||
forms `COM¹`/`COM²`/`COM³` and `LPT¹`/`LPT²`/`LPT³`, including extensions.
|
||
|
||
## LA11 — closeout
|
||
|
||
- One exact operator script
|
||
`docs/research/2026-08-14-campaign-la-test-script.md`, fronted by the
|
||
connection-free `tools/run-campaign-la-preflight.ps1` and followed by
|
||
serial user rows,
|
||
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.
|
||
|
||
## Gate round 1 — 2026-08-15 (first live launch by the user)
|
||
|
||
The user's first hands-on launch found the launcher exiting on every click.
|
||
Root cause (`d54b8a78`): `MainWindow`'s constructor called
|
||
`AvaloniaXamlLoader.Load(this)` instead of the generated
|
||
`InitializeComponent()`, so every `x:Name` backing field was null and any
|
||
modal open/close threw out of the dispatcher into `Program`'s exit-74
|
||
guard. It reached the gate because NO test constructed `MainWindow` —
|
||
filed and closed as **#399** (`2b439cc1`, merged): `Avalonia.Headless.XUnit`
|
||
view tests with falsification evidence (12/12 fail against the old code,
|
||
12/12 pass against the fix; launcher suite 66/66 Windows + native Ubuntu;
|
||
xunit→xunit.v3 in that test project). Same round (`e1e94697`): **#398**
|
||
closed — fatal exceptions now write a full-stack crash report under the
|
||
data root (isolated-roots-safe; the first cut leaked to the real data root
|
||
when parsing failed, caught live and fixed) — and `acdream-bake.exe` is now
|
||
co-deployed on plain Build, not just Publish, so a developer-built launcher
|
||
can actually run its first-run wizard (79.6 MB single file beside the
|
||
launcher, incremental, `--help` verified). One transient 65/66 on the first
|
||
post-merge test run did not reproduce across a clean rebuild + six repeats —
|
||
consistent with stale-artifact mixing, but if it EVER recurs, capture the
|
||
failing test name before anything else. Merged slice worktrees/branches
|
||
(la2/la3/la7a/la-uitest) removed. Opus batch review: PASS (HIGH
|
||
confidence) with 6 findings, all landed same-day: F1 the crash reporter's
|
||
by-construction claim was FALSE (the launcher holds passwords in three
|
||
fields; the true invariant — no throw site interpolates a credential
|
||
value — is now pinned by a forced-failure test), F2 the co-deploy's
|
||
Inputs covered only Bake's own sources, not its Content/Platform/Core/
|
||
Plugin.Abstractions closure (the stale-artifact class again; fixing it
|
||
exposed and fixed two more incrementality traps: SkipUnchangedFiles
|
||
leaving outputs older than inputs, and %(Item.Metadata) in a plain
|
||
Include not batching — a literal '%(...)' input is permanently
|
||
out-of-date), F3 dual bake publish on RID publishes (guarded by
|
||
_IsPublishing; verified 0 build-target co-deploys during a real publish),
|
||
F4 misattributed comment, F5 template-scoped x:Name false-fail (sweep now
|
||
walks the XML with template-ancestor tolerance), F6 dead using, plus the
|
||
optional Path.IsPathFullyQualified hardening on the crash reporter's
|
||
--data-dir fallback. Launcher 67/67, Launcher.Core 317/317. The §A–I
|
||
connected script remains the open user gate.
|
||
|
||
## Gate round 2 — 2026-08-15 (first live launcher→client flow) — char-select matrix USER-PASSED
|
||
|
||
**USER-PASSED 2026-08-15 (end of round):** the character-select visual/
|
||
interaction matrix — stretched-canvas look with bilinear filtering,
|
||
aligned widgets, left-justified roster, World box reading the live server
|
||
name ("sawato"), and the centered exit confirmation — all accepted on the
|
||
live launcher→client flow. Round-2 commits after the round-1 batch:
|
||
`6e1c0967` (session-config launches force the retail UI), `9ce72925`
|
||
(PFID_CUSTOM_RAW_JPEG decode + resolution guards), `73041d70`
|
||
(whole-canvas AD-98 scale + inverse input), `308f40a3` (linear-twin
|
||
bilinear stretch), `ef96c554` (exit confirmation + authored justify +
|
||
world name, AD-99), `2e6d69dd` (#400), `0a7dc7d6` (durable world-name
|
||
read + canvas-centered dialogs). Remaining before shipment: the formal
|
||
§A–I script rows (probe ×2, headless+plugins+login commands, delete/
|
||
restore, A→B update swap, row I Linux), and the final-HEAD preflight
|
||
re-run.
|
||
|
||
**Round REVIEW-CLOSED 2026-08-15:** the owed Opus dual-lens batch review
|
||
of the six round-2 commits returned PASS with 8 findings; the fix round
|
||
(`0baebce2` — headline: `RetailWaitDialogView` was the ONE dialog view the
|
||
EffectiveCanvasSize sweep missed, firing on ENTER; plus the two stale
|
||
deleted-mechanism doc assertions, the Confirmation `0xAC` property,
|
||
truncating input mapping, the IsCurrent world-name gate, the AD-98
|
||
evidence note) closed all seven in the narrow re-review; F2 filed as
|
||
#401 (invert RetailUi to opt-out). The review also proved the
|
||
`DatWidgetFactory` justify widening has ZERO regressions across all 35
|
||
layout fixtures (303 buttons swept; the 16 authored-Left all already
|
||
left-aligned via their face-child branch) and is a move TOWARD retail
|
||
(`CalcJustification @0x00467260` has no lifted-from-child condition).
|
||
|
||
Two real defects, both root-caused and fixed:
|
||
|
||
1. **`6e1c0967` — launcher-spawned clients had NO interface at all.**
|
||
`RetailUi` rode the dev env var `ACDREAM_RETAIL_UI`; `FromSessionConfig`
|
||
inherited the env parse; the launcher strips `ACDREAM_*` from children
|
||
(LA11 isolation). Product launches therefore got the dev default: world
|
||
rendering, zero UI — character screen included. Session-config launches
|
||
now force `RetailUi = true` (a session-config launch IS a product
|
||
launch); the env flag remains the dev-launch opt-in. Test-pinned with a
|
||
null env.
|
||
2. **`9ce72925` — character-select screen rendered magenta background/
|
||
fills.** The screen's 800×600 root background (`0x06007576`) is
|
||
`PFID_CUSTOM_RAW_JPEG` — a complete JFIF stream retail hands to the
|
||
Intel JPEG Library (`RenderSurface::CreateFromSourceData @0x004440a0`),
|
||
with Width/Height legitimately 0 on disk. `SurfaceDecoder` had no JPEG
|
||
case AND a non-positive-dimension guard, so it fell silently to the
|
||
magenta placeholder; the listbox/ENTER fills are transparent, so one
|
||
broken background bled through as three symptoms. Fixed via
|
||
StbImageSharp (managed, Linux-safe; codec-library substitution per the
|
||
BCnEncoder precedent — no register row). BOTH silent traps now log once
|
||
per id (id-resolves-but-undecodable in `SurfaceDecoder`;
|
||
id-missing-from-DATs in `TextureCache`) — the existing magenta guard
|
||
only covered id-0. New installed-DAT sweep asserts every char-select
|
||
media id decodes non-magenta. Full suite 14,034 green.
|
||
|
||
Session-orchestration facts this round: the machine gained PowerShell 7
|
||
(winget, user-approved — the LA fixture tooling hard-requires it); an
|
||
orphan feed server from the earlier session held port 43119 with stale
|
||
fixture data (stopped); the launcher self-update bootstrap restart on a
|
||
dev binary is EXPECTED (staged launcher update → exit → respawn).
|
||
Observations still open for this round: the duplicated "versioned client
|
||
is unavailable" status line (cosmetic), and verifying Create Character is
|
||
disabled on the live screen.
|
||
|
||
## Ledger
|
||
|
||
| Slice | Status | Commits | Review | Notes |
|
||
|---|---|---|---|---|
|
||
| LA0 | **DONE 2026-08-14** | `cb6502c8`, `a49e92df` | Opus dual-lens PASS; all 6 findings CLOSED in narrow re-review | Byte-identity proven; Linux CI lanes restored; Platform BCL-only self-guard added; K0 guard untouched |
|
||
| LA1 | **DONE 2026-08-14** | `db9ad53c` (mixed — see `e1322a06`), `75a6724d` (recovery WIP), `d511e4c3`, ledger `890cf267` | Initial review FIX FIRST; F1–F8 CLOSED; narrow dual-lens re-review PASS | Release build green (0 errors / 18 warnings). Windows: Runtime 1634 / Headless 127 / App 5038+3skip. WSL: Runtime 1634 / Headless 127. Known mid-play silent-wire-drop limitation recorded above. The LA1+LA3 composer-to-both-hosts contract gate and portable CI lane landed at `8a03a25f`. |
|
||
| LA2 | **DONE + MERGED 2026-08-14** | `c6019424` (recovery WIP), `000ea979`, `1c5e66c0`, merge `e01b2cd1` | Dual-lens review FIX FIRST; all 3 findings CLOSED; final narrow re-review PASS | Probe success requires a reported roster and remains before selection/EnterWorld; terminal status derives from the actual start outcome; conditional fields distinguish omission from explicit null without weakening strict JSON. Branch gates: Runtime 1,632/1,632 and Headless 149/149 on both Windows and Ubuntu/WSL. Integrated gates: Release solution build green; Windows Runtime 1,636/1,636, Headless 151/151, App 5,039+3 skip, Launcher.Core 114/114; WSL Runtime 1,636/1,636, Headless 151/151, Launcher.Core 114/114. Repeated live ACE probe remains the LA11 user gate. |
|
||
| LA3 | **DONE + MERGED 2026-08-14** | `37d74e44`, `26feba81`, `347a1a5d`, merge `7749545d`, seam `8a03a25f` | Initial 12 findings CLOSED; four-gap narrow review FIX FIRST; final narrow re-review PASS | `AcDream.Launcher.Core` remains BCL + Platform only. Windows/WSL Core 114/114; full Release build green. Composer output is parsed by BOTH real host loaders from one linked fixture; Launcher.Core build/tests run in the portable Windows+Ubuntu lane. Windows graceful-stop gap remains tracked as #397. |
|
||
| LA4 | **DONE + MERGED 2026-08-14** | `d0a9c65d`, `10a712d6`, `ae2cbbee`, merge `60f62799` | Initial dual-lens review found 10 issues; fix re-review left one Linux execute-bit gap; final narrow re-review PASS | Avalonia 12.1.1 launcher remains thin over one BCL-only Core orchestrator. Windows/WSL Launcher.Core 162/162 and Launcher 17/17. Native `linux-x64` publish evaluates self-contained + single-file, runs without a discoverable runtime, and CI verifies executable launcher/App/Headless artifacts. LA9/LA10 bodies and LA11 visual/accessibility confirmation remain intentionally later. |
|
||
| LA5 | **DONE + MERGED 2026-08-14** | `95f4be94`, `fbe9c8a2`, `f820eb25`, merge `5535d0ad` | Initial review found 5 issues; first narrow re-review found 4 ownership/race gaps; final narrow re-review PASS | Both hosts share exact absent/null=`all`, `[]`=`none` allow-listing; transactional scoped UI/entity/selection rollback precedes unload; graphical/headless status and teardown ordering match; headless replay is exact-once under Runtime's borrowed membership lease. Branch complete suite 13,679+4 skip; portable WSL closure green. |
|
||
| LA6 | **DONE + MERGED 2026-08-14** | `41b15efd`, `259f0e5a`, merge `2bb8ccb6` | Dual-lens/CH regression review found one Headless wire-parity gap; narrow re-review PASS | Runtime owns the sole parser/router/catalog and shared four-route live binding. Both hosts run generation-scoped login commands after world entry with strict monotonic delay and nonterminal v1 failure status. Headless permit/chat/notell semantics match App. Branch complete suite 13,787+4 skip; WSL Runtime 1,662, Headless 165, Launcher.Core 167, UI/chat 922. |
|
||
| LA7 | **DONE + MERGED 2026-08-14** | LA7a `6a32f375`, `4338b1c1`, `0c8643a7`, merge `fa2de1c4`; LA7b `0e82cbf7`, `1b9e7e41`, `ff406562`, merge `7691cf75` | LA7a retail-lens PASS; LA7b review found 4 issues, first narrow pass left one restore/delete interleave, final narrow re-review PASS; AD-97 filed | Runtime owns the sole generation-scoped pre-world selection graph. Exact retail roster/grey/button/delete/restore behavior and queue routing are preserved; `NumErrors` is a sentinel, paused selection retains reliable transport sweeping, silent restore cannot block, and App has no mirror. Windows Runtime 1,653, Core.Net 958, App 5,042+3 skip; WSL Runtime/Core.Net green. |
|
||
| LA8 | **DONE + MERGED 2026-08-14** | `6cfab727`, `aeac874d`, `1dd5706e`, merge `fe63ce18` | Initial retail/architecture review found 4 issues; first narrow re-review left 2 retry-transaction/order gaps; final narrow re-review PASS | Installed DAT enum table 5 proves `0x10000005 -> 0x21000004`, root `0x1000039A`, exact flat list/buttons/templates/dialog assets, and no viewport. Runtime remains the only selection owner; row sizing, modal priority/retry, restore ordering, reset/disposal, and explicit live-DAT skip/probe are covered. Branch full suite 13,796+5 skip; LA11 owns physical visual/live-ACE acceptance. |
|
||
| LA9 | **DONE + MERGED 2026-08-14** | `ff6ebb6a`, `3f688951`, `208a70ac`, merge `2198a0cc` | Initial integrity review found 5 issues; narrow re-review left one orphan-child publication race; final narrow re-review PASS | First-run installer validates four DATs, consumes strict v1 Bake JSONL, preserves/reverifies SHA+size+tool-version records, and co-publishes self-contained launcher+Bake. Cross-process install/publish locks plus durable nonce prevent post-recovery mutation across real parent-only hard kills on Windows/Linux. Branch full suite 13,799+4 skip; real retail-DAT bake remains LA11. |
|
||
| LA10 | **DONE + MERGED 2026-08-14** | `2d2a5b50`, `1955ca8a`, `09d84387`, merge `da4fb3de` | Initial architecture/security review found 10 crash, trust, integrity, cleanup, and lifecycle issues; first narrow re-review left one rollback-source P1; final narrow re-review PASS | Production feeds and redirects are HTTPS-only, fixture loopback trust is explicit, downloads and archives are bounded and verified, version activation and rollback are atomic, active sessions hold the cross-process update lease, and schema-v3 self-update recovery verifies every prior/replacement file before apply, rollback, or restart. Real Windows/Linux process tests cover kill boundaries, staging races, lease deferral, corrupt backups, junctions/symlinks, and fail-closed recovery. Branch gates: Core 302/302 and Launcher 29/29 on Windows/WSL, full Release 13,945+4 skip, win/linux self-contained publishes. Integrated LA0–LA10 gate: 13,972+5 skip. |
|
||
| LA11 | **AUTOMATED CLOSEOUT REVIEW-CLOSED + MERGED 2026-08-15 — USER GATE PENDING** | `f881e5b4`, `134edabe`, `accd01a0`, `9f9c1167`, merge `d39f3098` | Initial dual-lens review found 7 startup/evidence/safety issues; first narrow re-review left 2 PID-reuse/ZIP-mode gaps; final narrow re-review PASS | Strict isolated roots and process-local feed override compose one exact launcher path graph. Windows targeted CTRL_BREAK is group-isolated and preserves stdin; exact-PID/start-identity status validation, credential-value scanning, deterministic Unix-mode A/B fixtures, Windows/native-Linux helper safety, and the exact A–I operator script are implemented. Clean branch preflight passed 32/32 with 13,985 tests + 4 skips. Integrated clean-head preflight at `a22f5411` passed 32/32 with 14,012 tests + 5 skips and report SHA-256 `49f225bc6043b9256f17b7bf0f29df919c894b8355633077751fd279756470df`. No connected/UI/real-DAT row has run; campaign shipment and #397 closure remain pending the user gate. |
|