feat(launcher): stabilize prepared content updates
Some checks failed
CI / linux-portable (push) Failing after 3m12s
CI / windows-gate (push) Failing after 6m35s
CI / release (push) Has been skipped

This commit is contained in:
Erik 2026-08-25 19:17:13 +02:00
parent f160f3fee1
commit af9327a17b
42 changed files with 3706 additions and 147 deletions

View file

@ -0,0 +1,230 @@
# Launcher content stabilization
**Date:** 2026-08-25
**Status:** IMPLEMENTED
**Goal:** make prepared-content updates fast, explicit, and safe without
turning the launcher into a package manager.
## User contract
1. The launcher window appears before network access, full-file hashing,
baking, recovery, or any other potentially long operation.
2. Ordinary startup reads only small metadata: the install record, pak header,
file length/write time, and the verification sidecar when present.
3. No long content operation begins silently. The launcher first names the
reason, work kind, approximate disk requirement, and whether the existing
installed game remains usable.
4. A prepared-content change uses a small locally generated overlay whenever
the affected DAT IDs/landblocks are bounded. A full rebuild is an explicit,
rare fallback for format changes or extraction changes with unbounded
impact.
5. The launcher never starts a mixed client/content pair. Cancellation or a
bake/publication failure preserves the prior pair. Once approved content is
ready, an unavailable or failed matching-client update leaves Play disabled
and retains the verified content for a cheap retry.
## What exists already
- `acdream.pak` has a 64-byte header containing DAT iterations, format version,
and `BakeToolVersion` (the current content-recipe identity).
- `install.json` records the pak SHA-256, size, DAT path, and recipe identity.
- `install.verification.json` avoids the former 24-second startup hash when
size/write-time still match. Explicit **Verify files** remains the full-hash
path.
- `acdream-bake` already accepts `--ids` and `--landblocks`, and a filtered
bake produces an ordinary valid pak with only those typed keys.
- `IPreparedAssetSource` and `IPreparedCollisionSource` are the existing
renderer/physics seams; no consumer needs to know which mapped pak supplied a
key.
- Launcher and client are published together, and the launcher payload already
includes the matching bake executable.
## Deliberately small model
There are only four work kinds:
| Kind | Launcher behavior |
|---|---|
| `None` | No content prompt. |
| `Overlay` | Build one cumulative overlay containing all keys changed since the base recipe. |
| `FullRebuild` | Explain the long rebuild and required free space before starting. |
| `Verify` | User-requested or exceptional recovery hash; always visible and cancellable. |
The release-feed schema remains unchanged for the first implementation. Every
published build already updates the launcher before the client. The updated
launcher carries the matching content requirement and a small compiled
migration catalog. This avoids stranding strict schema-1 launchers on a feed
shape they cannot parse. A future independently versioned content feed can
replace the catalog without changing the runtime content model.
`BakeToolVersion` is retained on disk for compatibility but is treated as a
**content recipe version**, not an executable build number. It changes only
when the produced prepared content changes.
## On-disk content state
The existing `install.json` remains the base-pak authority and is not extended;
older launchers reject unknown fields. New state lives in the optional sidecar
`DataDirectory/pak/content.current.json`:
```json
{
"schemaVersion": 1,
"baseSha256": "<sha256 from install.json>",
"effectiveRecipeVersion": 6,
"overlay": {
"path": "acdream-update-6.pak",
"sha256": "<64 lowercase hex>",
"size": 123,
"recipeVersion": 6
}
}
```
Rules:
- The sidecar is valid only when `baseSha256` binds it to the current base
record and every path is a safe canonical filename beneath the pak directory.
- At most one overlay is active. A later overlay is cumulative and atomically
replaces the prior sidecar; there is no unbounded lookup chain.
- The base and overlay must name the same installed DAT iterations and pak
format. The base may carry an older recipe; the overlay carries the effective
recipe.
- Missing overlay keys fall through to the base. A present-but-corrupt overlay
key is authoritative corruption and never falls through.
- Render and collision reads follow the same ordering and share the same two
memory mappings.
- An absent sidecar means the base pak is the complete active content set.
- `content.client-pending` is a separate, tiny crash-safe activation gate. A
content migration creates it before touching content and removes it only
after client compatibility is confirmed. It deliberately carries no package
graph; existence means “do not publish this content to Play yet.”
## Migration catalog
One compiled catalog entry describes each recipe transition:
```text
target recipe
work kind
player-facing reason
affected DAT IDs and/or landblocks (overlay only)
```
To update a base from recipe 5 directly to recipe 7, the launcher asks the
catalog for the cumulative 5 -> 7 impact and emits one recipe-7 overlay. If any
step is `FullRebuild`, the combined migration is a full rebuild. A missing
catalog step fails closed with an explanatory error; it never guesses.
The recent procedural night-sky change is `None` because it changed client
shader/code only. A future addition of bounded prepared sky keys can be
`Overlay`. A global mesh-extraction correction such as recipe 5's solid-face
change is `FullRebuild`.
## Update transaction and UI
The launcher keeps the existing one-question update surface. When the candidate
client needs newer content, pressing **Update** first opens the content-work
confirmation:
> **World data update required**
> This release adds prepared sky assets. acdream will build a small update
> from your installed Asheron's Call files. The existing game stays installed
> until this finishes.
> Estimated work: overlay / approximately N files / M free space required.
> **Update now** · **Later**
After confirmation:
1. Validate the remembered DAT directory and free-space floor.
2. Build to a transaction-owned candidate path while the active content stays
untouched.
3. Validate pak header/TOC and compute the new artifact's SHA once. Never hash
the unchanged base as part of an overlay update.
4. Atomically publish the content sidecar.
5. Install/activate the compatible client.
The newly prepared content is not published to the launch orchestrator until
the startup check confirms that the active client is compatible or the client
update succeeds. Choosing **Not now**, losing the network, or failing the
client download therefore cannot launch the old executable against the new
pak. The launcher keeps the verified content on disk and resumes at the much
smaller client-update step.
For `FullRebuild`, the same transaction builds a candidate base beside the old
base, verifies it, then atomically swaps the base record/file. It never moves
the playable base out of place before the long build starts.
Progress uses the existing strict Bake JSONL protocol and shows phase,
percentage, failures, and ETA. Cancellation returns to the launcher without
changing active content.
## Startup ordering
`App.OnFrameworkInitializationCompleted` must not synchronously wait on
`LoadExistingAsync` before constructing `MainWindow`. It constructs the shell
with an explicit `Checking` installation state, assigns/shows the window, then
starts content discovery on the UI dispatcher. Feed update checking begins only
after that cheap discovery completes, preventing two startup modals from
racing.
If an exceptional recovery path really needs a full base hash, the shell is
already visible and says exactly what it is doing. Launch stays disabled until
the recovery check finishes, but the application never looks frozen.
## Compatibility and rollback
- A client session receives the resolved base path plus zero or one overlay
path. Old clients continue receiving only the base.
- The client validates the effective recipe before constructing world owners.
- The updater does not activate a client whose content requirement is
unsatisfied.
- Choosing **Later** leaves the old client/base pair active.
- Client rollback is allowed only when the selected client accepts the active
content set; otherwise the launcher explains the required content rollback
or rebuild instead of launching an incompatible pair.
## Verification gates
- Launcher window construction test proves no installer/hash task is awaited
before the main window is assigned.
- Quick-discovery tests cover missing sidecar, matching sidecar, missing cache,
changed length/time, recipe mismatch, and exceptional visible verification.
- Content-state tests cover path containment, base-digest binding, atomic
publication, cancellation, and crash residue.
- Composite-source tests cover overlay hit, base fallback, authoritative
overlay corruption, render/collision parity, stats, and balanced disposal.
- Session-config round trips cover base-only and base+overlay on App and
Headless.
- Update tests prove prepared content cannot become launchable before client
compatibility is confirmed; **Not now** and client-download failure remain
fail-closed, while bake/candidate failure preserves the prior pair.
- Release solution compilation and the affected Launcher, Content, App, and
Headless gates remain green.
## Implementation checkpoint
Implemented 2026-08-25:
- The Avalonia window is assigned and opened before content discovery, client
recovery, feed access, or exceptional hashing begins.
- Ordinary current-install discovery uses metadata/header/cache checks; the
explicit verification command owns visible whole-pak hashing.
- Recipe migrations are compiled and cumulative. Bounded migrations build one
filtered overlay; unbounded/global migrations use the explicit candidate
full-rebuild path. The current recipe 4 -> 5 transition is correctly a full
rebuild because the solid-face extraction change is global.
- Base/overlay reads are unified for render and collision with overlay-first,
Missing-only fallback and authoritative corruption.
- Content activation is bound to client compatibility in memory and through
`content.client-pending`, so **Not now**, failed download, process crash, and
launcher restart cannot expose a mixed pair.
Final Release gates:
- `dotnet build AcDream.slnx -c Release`: 0 warnings, 0 errors.
- Launcher UI/ViewModels, excluding the documented manual desktop lane: 82/82.
- Launcher.Core Windows-compatible suite: 360/360.
- Hermetic Content suite: 130/130.
- Affected App layered/session composition: 35/35.
- Affected Headless configuration: 9/9.