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

@ -332,7 +332,19 @@ src/
Installation/ -> portable four-DAT validation, Windows retail
path discovery, versioned JSONL bake-process
orchestration, and atomic SHA/size/tool-version
install-record verification and recovery; one
install-record verification and recovery;
startup discovery begins only after the desktop
window opens, and exceptional whole-pak hashing
reports its long-read status in that window;
content recipes resolve through one compiled
None/Overlay/FullRebuild/Verify migration ledger;
bounded changes build one cumulative filtered
overlay and publish `pak/content.current.json`,
while full rebuilds bake beside the live base and
swap only after candidate verification; a tiny
`pak/content.client-pending` gate survives a
crash/restart until the active client is
confirmed compatible; one
OS-handle lease serializes recovery/install per
DataDirectory; a second OS-held publication
lock plus durable per-transaction nonce makes
@ -362,7 +374,9 @@ src/
is never persisted, and permits HTTP only for a
loopback fixture; production remains pinned HTTPS
ViewModels/ -> thin MVVM projection over Launcher.Core,
including the first-run DAT/bake wizard and
including the first-run DAT/bake wizard, explicit
world-data work confirmation (kind, reason,
free-space guidance, progress/cancellation), and
nonfatal startup/manual update state, actions,
progress, cancellation, rollback, and errors
-> references Launcher.Core only (Platform transitively); it never owns
@ -372,6 +386,19 @@ src/
-> Linux launcher/probe/headless flows remain portable; graphical-client
actions are explicitly disabled until Modern Runtime Slice L resumes
Prepared-content launch contract
-> `install.json` remains the strict backward-compatible base-pak authority
-> optional `pak/content.current.json` binds one cumulative overlay to the
base SHA; there is never an unbounded overlay chain
-> `pak/content.client-pending` prevents newly migrated content from
becoming launchable before the matching client check/install succeeds,
including across launcher restart
-> launcher session config carries base + optional overlay paths and both
recipe identities only for layered launches
-> App and Headless construct one `LayeredPreparedAssetSource`; overlay
Missing falls through to base, while overlay Corrupt is authoritative
for both render and collision reads
AcDream.Headless/ Linux/Windows no-window production host
Program.cs -> CLI entry only
Configuration/ -> strict versioned process/session config

View file

@ -132,6 +132,20 @@ bake/equivalence/UI-Studio tools, not a production fallback. Portal → HighRes
installed-DAT gates are recorded in
`docs/research/2026-07-24-slice-c-prepared-asset-cutover-report.md`.
**Launcher cumulative-overlay extension (2026-08-25).** Production still has
no live-DAT fallback and consumes the same prepared-payload contracts. For a
bounded recipe migration, App and Headless may receive one complete base pak
plus one cumulative filtered pak through `LayeredPreparedAssetSource`. The
overlay is probed first: Missing falls through to the base, while a present but
corrupt render or collision payload remains authoritative corruption. Both
mapped owners share one composite lifetime and there is never an overlay
chain. The launcher binds the overlay to the base digest in the optional
`pak/content.current.json` sidecar; format/global extraction migrations retain
the explicit full-rebuild path. A tiny `pak/content.client-pending` marker
keeps either result non-launchable until the matching client is confirmed,
including across a crash/restart. Design and gates:
`docs/plans/2026-08-25-launcher-content-stabilization.md`.
**Slice I3 prepared collision extension (2026-07-25).** The package remains
format 1 and retains mesh type values 13; bake-tool 4 appends typed GfxObj,
Setup, CellStruct, and EnvCell-topology collision payloads. Core owns the

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.