The zone walk and the runtime were the same types. A catalog carried the
walk's `Ptr → row` link maps, its half-captured names and its scratch into
the published result, so nothing said the linking was over except a
convention — and the runtime, which has no zone open any more, could not
have used them anyway.
Build and published types are now separate for materials, effects, tracers
and reticles: `MaterialDefinitions`, `FxDefinitions`, `TracerDefinitions`
are what a finished build hands on, `publish` is the only way to get one,
and the pointer lookups and absorbs stay behind on the builder. The
presence those pointers stood in for is one shared type, `AuthoredRef {
slot, alias }`. Every published asset type is free of zone pointers except
`WeaponCombatFx`, which waits on splitting `WeaponRegistry` the same way.
That split also turned up dead code the old types had hidden:
`resolve_after_absorb` looked weapon icons up by pointer after `finalize`
had emptied the map, so every lookup there returned `None`.
Map load is a transaction with one terminal outcome. Preflight assembles a
`MatchInstallPlan` — nothing to publish exists until every check that can
refuse has passed — and any refusal writes `MapLoadFailed` for the request,
including "no drawable world", which used to be a `MatchInstalled` the
lifecycle read as success and the UI read as failure. A canceled walk
returns `MatchLoadOutcome` instead of orchestrating on with empty products.
The match carries one material population (`PreparedMatch.materials`)
rather than reaching for it through geometry that may not exist, and the
transport schema around the boundary — the six `LoadCapture` wrappers,
`PreparedSpawn`, the doubly-owned map facts, `ZoneMemory` held to the end
of install — is gone.
Copies that had no reason to exist: the absorbs that already consume their
donor now move its rows instead of cloning every `ModelSkel` and
`RawXAnimParts`; sim and the dyn-ent physics share one `ClipMeshTables`
behind an Arc instead of each holding a deep copy of the collision.
Ownership on the live side follows the same cut. `SessionCore` takes a full
room identity and revision before any transport effect, so a stale
teardown cannot override a different match key. `HostMatchEvent::Applied`
carries no match key, so the peer's phase is its whole guard — it now
passes before anything is written, instead of landing a retired match's
connection id on the live peer on the way to being refused. `ClipStore` owns every PCM
conversion and every terminal decode failure, with Play/Stop on one ordered
channel scoped by namespace, entity and epoch. `SimContent` holds the
immutable definitions behind a consumed builder, so authority and
prediction share one backing instead of deep-copying per tick. Direct UDP
launch is removed; relay, listen and replay stay.
In the renderer each producer publishes whether its rows actually moved,
and the xmodel merge stops re-hashing seven producers' draws, materials and
vertex counts every frame to find out. Payload fields are `pub(crate)`
behind read-only accessors, so a consumer that wanted to finish a
producer's work no longer compiles. Colour extract is split from the
installed world, scene and shadow get their own bind caches, and record
census stays off the hot refusal path.
Docs: `CONTEXT.md` gains "Before shipping: the scaffolding goes back out",
and `make mr` and `cargo xtask mr` point at it. MAP-LOAD describes the
transaction as it now ends.
Scaffolding: the share and census probes that only proved the split are
out. What stays is five tests, each holding several independent systems to
one claim and each verified to fail when that claim is broken:
- one alias carried from two-zone capture through absorb, edge binding,
the alias-chain walk, the prep workers' decode and finally cancel —
every stage has to mean the same clip by it;
- a replacement match driven through all three cores that decide whether
a player is in one (host admission, session view, local load), then
every event the retired match still had in flight, replayed against a
live peer, moving nothing;
- a rejected room view leaving no bridge identity, membership, bootstrap
or admission effect behind;
- the seven draw producers published through their own API and asked of
the consumer that replaced the merge's per-frame hashing: the stamp
moves for exactly the producers whose rows moved;
- authority and prediction proved to share one immutable world by
tracing it, and to keep tracing their own after the other replaces it.
3.5 KiB
Map load: what lives where
Three floors: disk (asset_transport), format (fastfile_* → the
asset_iw4 IR), products (asset_* + assets). assets knows the disk
through the transport and knows nothing about the format; fastfile_* knows
the format and opens no files. Installing the match is not here — it is in
session.
The transaction: who calls whom
console/ui `map` → session::lifecycle SessionSwapRequest → tear down the previous
occupancy, then MapLoadApproved{request_id}
→ assets::match_load approve_map_load find_zone_file / find_runtime_common_mp
start_match_load spawn onto load_pool() (MatchLoadBusy)
poll_match_load → PreparedMatchReady
→ session::match_apply apply_prepared_match → scene + SimWorld + MatchInstalled
Cancellation is MatchLoadAbort(request_id): the walk returns
MatchLoadOutcome::Canceled at its next checkpoint and there is no prepared
match to throw away. Failure is MapLoadFailed — from discovery and from
install; LoadingScreen holds the failed install until teardown and cannot
turn it into a success. Every accepted request ends once: MatchInstalled or
MapLoadFailed, never both.
The walk itself: assets::session_load::load_prepared_match
The single entry point; the composition root sees neither fastfile_* nor
AssetSink. Inside are several parallel walks on load_pool(): opening the
archive, startup materials, common_mp, the IW5/T5 weapon bundle, language
zones. common_mp goes before the map: its techset tables are needed
before build_world_draw. The output is MatchLoadOutcome::Ready with a
PreparedMatch (world, one MatchMaterials population, clip, weapons,
catalogs, PreparedMap — the single owner of spawns and map facts, report).
Ownership rules the walk keeps: the zone arenas die with the walk that reads
them (only their size travels), a consumed donor catalog is moved row by row
rather than cloned, and the merged material population is finalized once —
whether or not the map has a GfxWorld — and never lives inside the geometry.
The per-game adapter is assets::lane (ZoneGame → iw4/iw5/t5), the only
exhaustive match ZoneGame in the project; the runtime stays game-blind. A gap
in the lane is a typed LaneGap, not silence.
Installing it: session::match_apply
Two halves. Preflight builds a MatchInstallPlan: mode, doors, objectives,
scene conversion, drawable world. Nothing of the match is published until it
hands one over, so an ordinary refusal leaves no half-installed resources
behind and completes the swap with MapLoadFailed. The commit half then
publishes the plan, boots the sim and writes MatchInstalled.
Cache
iw4l-artifacts/cache/<kind>/<key>, content-addressed, leaf in
asset_transport::artifact_cache (cache_get / cache_put, fnv1a64). A
miss is silent: the caller computes the value anyway, and a hit is obliged to
be the same bytes a miss would have written (A2). The key names every
input; if the encoder changed, bump the format word at the caller. Live kinds:
mips (asset_material/material_images.rs), wgsl
(render_frontend/.../wgsl_disk_cache.rs).
How to poke it
IW4L_GAMES is the root holding the game trees; no folder name is hardcoded
anywhere (T1); the container version picks the decoder, not the folder name
(T2). Live, it is make map mp_boneyard (RUN.md); the stages of the
walk are visible in LoadProgress and in the .pftrace (PERF.md).