Asset loading

Usage guide of asset.preload, asset.cache and asset.meta: the loading plan in phases, the device cache, asset passports, settings, hooks, modding, engines.

Source: module-kits:kits/asset_preload/usage/en.mdView Markdown source

Also read offline — CLI: grip-forge docs asset-preloadMCP tool: gripforge_docs { "topic": "asset-preload" }

Asset loading, cache and metadata (asset.* kits) #

How the files of a game reach the player: loaded in the order the game needs them, kept between two visits, and described by their passport. Readable offline: gripforge docs asset-preload, or the MCP tool gripforge_docs (topic asset-preload).

The map #

KitRole
asset.preloadloads each file once, in phases (first, background, next, lazy), and reports progress; draws nothing
asset.cachekeeps the files on the player's device so a second visit downloads nothing that did not change
asset.metahands the passport of each asset (real size, colliders, skeleton, per-kit sections) to the other kits as plain data

All three require only game.engine and are services (role service). asset.preload reads asset.cache before downloading and writes to it afterwards. The loading view is another module: the title screen of ui.menu, ui.endscreen between two levels, the bar of hud.action (loadingBar: true), or the page.

Turn it on #

gripforge_gamekit_install   { project_id: "gkp_…", id: "asset.preload" }
gripforge_gamekit_install   { project_id: "gkp_…", id: "asset.cache" }
gripforge_gamekit_configure { project_id: "gkp_…", id: "asset.preload", config: { "aheadScenes": 2, "concurrency": 4 } }

Only the keys that differ from the defaults need to be written. The full reference of a kit is gripforge kit api <id>.

asset.preload #

The plan #

Every file goes in one phase:

PhaseWhatWhen
firstwhat the first playable frame needs: the files of the start scene; every file of a game without scenesbefore the game starts
backgroundthe rest of the gamewhile the game is played
nextthe files of the scenes that may come next (nextScenes: "auto", aheadScenes)after background, and again at each scene entered
lazynothing aheadonly when something asks for the file

Inside a phase, higher priority first, then the order of the game. One download per file: every caller gets the same promise while it downloads, then the same bytes (the page, the web player, several kits, several labels of one glTF). A file with a SHA-256 is read from asset.cache first, checked (size, SHA-256, then the host's format check) and kept for the next visit. A request jumps the queue.

Settings and defaults #

KeyDefaultMeaning
enabledtrueoff: the plan is empty; files are loaded only when asked (still once each)
defaultPhasefirstthe phase of a file the plan cannot place (first: the game starts once every file is in)
nextScenesautoauto loads the scenes after the current one ahead; none
aheadScenes1how many scenes ahead (0 to 8)
concurrency3downloads at once (1 to 8)
memoryMB512megabytes of downloaded files kept for late callers
verifytruecheck the SHA-256 of every file that has one, also from the cache
progressEveryMs250at most one progress event per this many ms
rules[]patterns placed by the game before the documents, e.g. { match: "assets/hud/*", phase: "first", priority: 5 }

The game shapes the plan with documents of the collection asset_preload (moddable, so a mod does the same):

{ "id": "boss_early", "asset": "boss", "phase": "first", "priority": 10 }
{ "id": "intro_movie", "asset": "assets/intro.webm", "remove": true }
{ "id": "extra_music", "path": "assets/music/night.ogg", "sha256": "…", "bytes": 4200000, "phase": "next", "scene": "night" }

State, events, service #

Variables: asset.preload.phase (idle, first, background, next, done), asset.preload.progress (0 to 1), asset.preload.first_ready, asset.preload.source (cache, network, mixed). Events: asset.preload.planned, .phase, .progress (phase, ratio, first_ratio, downloaded_bytes, total_bytes, verified, count, from_cache, from_network, source), .loaded, .failed, .done, .ahead, .started, .complete, .released. Service: plan(), progress(), request(path, priority?), prioritize(path, priority, phase?), prefetch(scene), cancel(path?), ledger().

Hook asset.preload.plan { id, path, phase, priority, scene, bytes }: phase and priority can change; cancelled, the file leaves the plan and is loaded only when asked. A mod may override enabled, defaultPhase, nextScenes, aheadScenes, concurrency, memoryMB, rules and add documents to asset_preload.

asset.cache #

The store is IndexedDB in a page, else Cache Storage with a service worker scoped to the game, else the HTTP cache through the servers' headers, else memory or nothing. The key is the path and the SHA-256 (assets/scene.glb@<sha>): a new version has another key, so it is downloaded and the old entry of the path is removed. A file without a checksum is never kept. A browser that refuses storage gives mode: off and the game plays, each file still downloaded once.

KeyDefaultMeaning
enabledtrueoff: nothing is kept
quotaMB512past it, entries are evicted (a file larger than the quota is not kept)
version1changing it empties the store at the next visit
storeautoauto, indexeddb, cachestorage, http, memory, off
fallbacksindexeddb, cachestorage, httpstores a page may use, in order
evictionlrulru (read longest ago), fifo, none
serviceWorkertrueserves kept files to every request of the game
minBytes0smaller files are not kept

Service: stats() (mode, entries, bytes, quota, hits, misses, writes, evicted), entries(), invalidate(path), clear(), write, read. Variables asset.cache.mode, asset.cache.bytes, asset.cache.entries. Hooks asset.cache.store { path, sha, bytes } (cancel: not kept) and asset.cache.evict. A mod may override enabled, quotaMB, fallbacks, eviction, minBytes. The servers answer Cache-Control: immutable for a name that carries its version, and an ETag (the SHA-256) otherwise.

asset.meta #

Every asset carries a mesh passport (lib_….gfmesh.json, next to the file in the library): real size, orientation, skeleton, colliders, clips, and one section per game kit (kits.<section>, owned by the kit that reads it; the first is lod for render.lod: triangles, radius, levels, category, impostor, ratios, screenSizes, shadow levels). The measured values are auto, the user's corrections are manual and always win; the game receives the resolved passport.

Passports are documents of the project collection asset_passports, one per bound slot, filled from the library when the web bundle or an engine delivery is built:

{ "id": "tree", "item": "lib_…", "kind": "prop", "name": "Old oak",
  "passport": { "etalon": { "height": 7.4 }, "kits": { "lod": { "category": "vegetation", "triangles": 18200, "radius": 4.1 } } } }

The kit has no config. Service: of(ref) (the resolved passport or null), section(ref, name), info(ref), has(ref), ids(), register(id, passport, info), bind(entity, id), reload(); ref is an entity id, a slot id or a library id. Event asset.meta.changed { id }. A kit that reads it must work without it: an asset without a passport answers null.

const lod = services.get('asset.meta')?.section(entityId, 'lod'); // null: use your own defaults

Nothing is moddable: a passport is the measured truth of an asset, so a mod that adds or patches an asset_passports document is refused (collection_not_moddable). A mod changes what the game does with an asset, not what the asset is.

Engines #

KitWeb and native playerGodot, Unity, Unreal
asset.preloadthe loader of the page and the Rust plan, hook and stateeach keeps its own loading
asset.cacheweb and Rustnone
asset.metaall featuresdelivered natively, same passports

When it does not do what you expect #

SymptomCause and fix
The game waits for every file at the startdefaultPhase is first and no scenes split the plan: move files to background with asset_preload or rules
The second visit downloads againthe file has no SHA-256, asset.cache is off or its store is unavailable (stats().reason), or version changed
A file is loaded when asked onlyits phase is lazy, or an asset.preload.plan tap cancelled it
The loading bar never showsthe view must read asset.preload.progress; hud.action needs loadingBar: true
of(ref) answers nullthe asset has no passport; the kit must use its defaults