diff --git a/docs/plans/2026-08-19-launcher-usability-campaign.md b/docs/plans/2026-08-19-launcher-usability-campaign.md index 8d379d04..2d1b9dd6 100644 --- a/docs/plans/2026-08-19-launcher-usability-campaign.md +++ b/docs/plans/2026-08-19-launcher-usability-campaign.md @@ -1,26 +1,78 @@ # Campaign LU — launcher usability -**Status:** CODE-COMPLETE 2026-08-19. All six slices implemented, tested, and -committed locally (not pushed — the user asked for local verification first). -The connected user gate is the remaining boundary. +**Status: CLOSED USER-ACCEPTED 2026-08-19/20.** Ten slices — the six planned +plus four the gate rounds added — shipped through CI and accepted live. -| slice | commit | tests | +**Gate results, in the user's words:** the update flow "works, it updates as it +should"; the launcher self-update round "pass"; the client's exit back to the +character selector "pass". + +| slice | commit | what it fixed | |---|---|---| -| (blocker) #420 client crash | `a34e8f2a` | reverting test reproduces the live stack | -| LU1 instant startup | `00d12782` | 10 cache tests + 2 view-model tests | -| LU2/LU3 one update question | `a01ff426` | 8 rewritten update tests | -| LU4 Setup complete | `0a2defb6` | 2 tests (success + failure never claims success) | -| LU5/LU6 Play + sessions | `09305be6` | 16 session-row tests | +| (blocker) #420 client crash | `a34e8f2a` | character select killed the client mid-paint | +| LU1 instant startup | `00d12782` | 29.9 s → 0.89 s, measured on the real 27.9 GiB pak | +| LU2/LU3 one update question | `a01ff426` | six buttons → Update / Not now, self-restarting | +| LU4 Setup complete | `0a2defb6` | setup ends with a dialog, not a finished progress bar | +| LU5/LU6 Play + sessions | `09305be6` | one Play per character; rows say who is playing | +| (cross-cutting) locale | `6a15dd06`, `955c6180` | retail text stopped following the machine's locale | +| headless CLI + LU7 | `2bff44a9` | headless and character refresh had never run at all | +| LU8 roster + fold | `18bbd377` | logging in IS the refresh; Play above the fold | +| LU9/LU10 stop + logout | `6ab5d8ce` | 30 s graceful stop, ACE hold, logout lands on select | +| verification-cache limit | `7037681a` | the ZFS finding below | -Full solution under the release-gate filter: **14,370 passed, 0 failed, -0 skipped.** +Full solution under the release-gate filter: **14,375 passed, 0 failed, +0 skipped**, and identical under `sv-SE`, `tr-TR`, `ar-SA` and `de-DE`. -**Known gate limitation.** The client installed under -`%LOCALAPPDATA%\acdream\app\` is the pre-#420 build, so pressing **Play** still -crashes on the character-select screen until a release carrying the fix is -published and the launcher installs it. That is itself the natural next test: -push → CI publishes → the new startup prompt offers the update → taking it -exercises LU2 end to end and replaces the crashing client. +--- + +## What the gate rounds found that the plan did not + +Four of the ten slices did not exist when this plan was written. Each came from +the user running the thing, and each was a defect the automated suite could not +have surfaced: + +**Headless and character refresh had never worked, once.** The launcher spawned +`acdream-headless --config `; the host reads `arguments[0]` as its command +and accepts only `validate` or `run`. Every launcher-started headless session +and every roster refresh died on its first instruction with "Invalid command" +and exit 64 — visible only as a code in a status file. A whole campaign's gates +missed it because they drove the headless host through its CLI directly, never +through the launcher's spec. `LauncherHeadlessCommandLineContractTests` now +feeds the launcher's real argument vector to the host's real parser. + +**Refresh was harmful as well as broken.** It opened a second connection to an +account purely to read the roster, which the server treats as a new login — so +using it while playing disconnected you. It was also redundant: every ordinary +login already carries the roster, and the orchestrator already folds it in. + +**Stop was the crash.** The UI gave the client five seconds before killing it, +which is not enough to send a logout, await the acknowledgement, and tear down a +mapped 28 GB world. So Stop routinely produced exactly the ungraceful exit that +leaves the server holding the account. + +**Play was below the fold.** The buttons existed; the plugins/login-commands +form pushed them past the bottom of the scroll area. Reported, correctly, as +"there is no headless or gui option". + +## Findings worth keeping + +**The verification cache cannot see a same-size, same-timestamp change.** Run +174 failed on a test asserting it could. Measured on the runner: `/tmp` is ZFS, +and 141 of 200 same-size rewrites produced an identical mtime. NTFS's 100 ns +resolution is the only reason it never showed on Windows. The contract is now +two true statements — startup catches a corruption whose write time moves, and +a forced full verification catches one that preserves both — instead of one +that is false on some filesystems. Verify files is the forced path. + +**Testing the launcher does not test your source.** The launcher runs the +INSTALLED client from the version store, so a client-side fix cannot be gated +until CI publishes it. A void-world screenshot was read as "the fix failed" when +the installed build was 63 minutes older than the fix. + +**A locally built launcher cannot test self-update.** Its stamped version is +`1.0.0`, which sorts above every `0.1.0-build.*` the feed publishes, so it is +never offered an update. Publishing one with a deliberately low +`InformationalVersion` is what made that path testable at all. **Goal** diff --git a/src/AcDream.Launcher.Core/packages.win-x64.lock.json b/src/AcDream.Launcher.Core/packages.win-x64.lock.json index 0f43b377..ed209493 100644 --- a/src/AcDream.Launcher.Core/packages.win-x64.lock.json +++ b/src/AcDream.Launcher.Core/packages.win-x64.lock.json @@ -1,7 +1,17 @@ { "version": 2, "dependencies": { - "net10.0": {}, + "net10.0": { + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[10.0.8, )", + "resolved": "10.0.8", + "contentHash": "dVbSXGIFNR5nZcv2tOLoWI+a9T4jtFd77IYjuND+QVe360qWgAF7H0WtoopYhRw/+SgpGUTyrkrh+65+ClNnfw==" + }, + "acdream.platform": { + "type": "Project" + } + }, "net10.0/win-x64": {} } } \ No newline at end of file diff --git a/src/AcDream.Platform/packages.win-x64.lock.json b/src/AcDream.Platform/packages.win-x64.lock.json index 0f43b377..c0ad5490 100644 --- a/src/AcDream.Platform/packages.win-x64.lock.json +++ b/src/AcDream.Platform/packages.win-x64.lock.json @@ -1,7 +1,14 @@ { "version": 2, "dependencies": { - "net10.0": {}, + "net10.0": { + "Microsoft.NET.ILLink.Tasks": { + "type": "Direct", + "requested": "[10.0.8, )", + "resolved": "10.0.8", + "contentHash": "dVbSXGIFNR5nZcv2tOLoWI+a9T4jtFd77IYjuND+QVe360qWgAF7H0WtoopYhRw/+SgpGUTyrkrh+65+ClNnfw==" + } + }, "net10.0/win-x64": {} } } \ No newline at end of file