Follow-up to the retail-text fix. "Green under sv-SE" is not the same as "runs
on any locale", so this establishes the latter by running the suite under
cultures chosen to break different things, and fixing what they broke.
ar-SA found a genuine defect the Swedish runner cannot see: the resolution
parser read "1920x-1" through the ambient culture, and ar-SA's negative sign is
not ASCII '-', so the parse failed and the height silently became 0 instead of
-1. Both copies of that parser (App settings targets and the UI settings store)
now parse invariantly.
Audited every remaining culture-sensitive operation in src/ rather than fixing
only what a test happened to catch:
- Numeric Parse/TryParse with no IFormatProvider: 11 sites, all reading
MACHINE-readable input — env vars (ACDREAM_LIGHT_DEBUG, ACDREAM_NET_DROP_*,
streaming/quality knobs), CLI arguments, "1920x1080" settings keys, a chat
command's price argument, and the launcher's bake thread count, which is
handed straight to a child process command line. All pinned to
InvariantCulture.
- ToUpper()/ToLower() with no culture: none. The Turkish-I class was already
clean, and tr-TR confirms it.
- StartsWith/EndsWith/IndexOf(string) with no StringComparison: one —
ChatInputParser's "@" prefix test, which is a culture-sensitive comparison
for a single ASCII character. Now the ordinal char overload.
Verified: 13,958 tests pass identically under the machine default, sv-SE,
tr-TR, ar-SA, and de-DE. (The two launcher test assemblies are excluded from
this run only because a running acdream-launcher.exe holds its own binary; the
one launcher change here is the thread-count parse.)
Dates remain on the current culture by intent, unchanged from the previous
commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Setup used to finish by leaving a full progress bar and a status line on
screen, with the same Validate / Cancel bake / Close / Build and install row
underneath. Nothing said "you are done" and nothing said what to press.
The wizard now swaps its whole form for a plain completion panel: "Setup
complete", one sentence saying the content was built and verified, and a single
OK that closes the dialog and returns to the launcher.
Raised at exactly one point — after _onInstalled publishes the record — so the
launcher behind the dialog is already in its launch-enabled state when OK is
pressed, and the "Client setup required" banner is gone the moment the user
gets back. The cancelled and failed branches deliberately never reach it and
keep their existing status/error reporting.
Tests: FirstRunSetupEndsWithACompletionPanelThatOkReturnsFrom (form hidden,
panel shown, record published before OK, wizard reopens as an ordinary form
afterwards) and AFailedFirstRunSetupNeverShowsTheCompletionPanel.
Launcher 61 passed.
Campaign LU slice LU4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured on the user's machine: %LOCALAPPDATA%\acdream\pak\acdream.pak is
29,908,271,024 bytes and SHA-256 over it takes 24.1 s at 1.16 GB/s. App
.OnFrameworkInitializationCompleted ran exactly that hash synchronously,
before constructing the window, and the digest came back identical to the one
install.json already recorded. So the launcher took roughly half a minute to
appear in order to re-confirm a fact that had not changed. A friend does not
see it only because they have no package installed yet — verification
short-circuits at "nothing installed" — so it would hit them the moment
first-run setup finished.
Startup now checks the cheap facts (size, last-write time) and skips only the
hash, and only when a previous FULL hash of that same file agreed with the
install record. Everything that should hash still does: install, update, the
crash-recovery backup path, and a new explicit "Verify files" button.
The remembered fact lives in a SIDECAR (install.verification.json), not as a
new field on the install record: LauncherInstallRecordStore reads install.json
with JsonUnmappedMemberHandling.Disallow, so a new property there would make an
older launcher build reject the record outright and demand a fresh ~28 GB bake
after a rollback. An unknown sidecar is simply ignored by builds that predate
it. The cache type never throws — it sits in front of a guarantee, so every
failure mode (missing, corrupt, unknown schema, unwritable) degrades to
"hash it again" rather than to a failed launch.
Two subtleties worth keeping:
- The write time is re-read after the hash and the entry is only written when
it is unchanged. A writer racing a multi-second hash would otherwise be
remembered under the OLD timestamp, and the next startup would trust a
digest that never covered those bytes.
- A hash that disagrees with the record invalidates the entry, so a stale
"verified" fact cannot outlive the evidence that produced it.
Tests: PreparedAssetVerificationCacheTests (10) counts hash invocations through
the store's injectable hasher and covers second-startup skip, forced full
verification, touched package, same-size silent corruption, resize, a cache
digest that disagrees with the record, three unreadable-cache shapes, and
backup recovery still hashing. Plus two LauncherWindowViewModel tests for the
Verify files command. Launcher.Core 335 passed, Launcher 57 passed.
Note for the first run after this ships: the very first startup still pays one
full hash to learn the digest for the installed file, and every startup after
that is instant.
Campaign LU slice LU1. Plan: docs/plans/2026-08-19-launcher-usability-campaign.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>