feat(launcher): stabilize prepared content updates
This commit is contained in:
parent
f160f3fee1
commit
af9327a17b
42 changed files with 3706 additions and 147 deletions
230
docs/plans/2026-08-25-launcher-content-stabilization.md
Normal file
230
docs/plans/2026-08-25-launcher-content-stabilization.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue