230 lines
10 KiB
Markdown
230 lines
10 KiB
Markdown
# 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.
|