Files
iw4L/docs/MAP-LOAD.md
T
vtroc 7dcdd6831e Give every loaded population one owner, and publish it without the walk that built it.
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.
2026-09-13 23:23:58 +03:00

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).