# 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 | Kit | Role | | --- | --- | | `asset.preload` | loads each file once, in phases (`first`, `background`, `next`, `lazy`), and reports progress; draws nothing | | `asset.cache` | keeps the files on the player's device so a second visit downloads nothing that did not change | | `asset.meta` | hands 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 ```text 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 `. ## asset.preload ### The plan Every file goes in one phase: | Phase | What | When | | --- | --- | --- | | `first` | what the first playable frame needs: the files of the start scene; every file of a game without scenes | before the game starts | | `background` | the rest of the game | while the game is played | | `next` | the files of the scenes that may come next (`nextScenes: "auto"`, `aheadScenes`) | after background, and again at each scene entered | | `lazy` | nothing ahead | only 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 | Key | Default | Meaning | | --- | --- | --- | | `enabled` | `true` | off: the plan is empty; files are loaded only when asked (still once each) | | `defaultPhase` | `first` | the phase of a file the plan cannot place (`first`: the game starts once every file is in) | | `nextScenes` | `auto` | `auto` loads the scenes after the current one ahead; `none` | | `aheadScenes` | 1 | how many scenes ahead (0 to 8) | | `concurrency` | 3 | downloads at once (1 to 8) | | `memoryMB` | 512 | megabytes of downloaded files kept for late callers | | `verify` | `true` | check the SHA-256 of every file that has one, also from the cache | | `progressEveryMs` | 250 | at 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): ```json { "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@`): 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. | Key | Default | Meaning | | --- | --- | --- | | `enabled` | `true` | off: nothing is kept | | `quotaMB` | 512 | past it, entries are evicted (a file larger than the quota is not kept) | | `version` | 1 | changing it empties the store at the next visit | | `store` | `auto` | `auto`, `indexeddb`, `cachestorage`, `http`, `memory`, `off` | | `fallbacks` | `indexeddb`, `cachestorage`, `http` | stores a page may use, in order | | `eviction` | `lru` | `lru` (read longest ago), `fifo`, `none` | | `serviceWorker` | `true` | serves kept files to every request of the game | | `minBytes` | 0 | smaller 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.
`, 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: ```json { "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. ```ts 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 | Kit | Web and native player | Godot, Unity, Unreal | | --- | --- | --- | | `asset.preload` | the loader of the page and the Rust plan, hook and state | each keeps its own loading | | `asset.cache` | web and Rust | none | | `asset.meta` | all features | delivered natively, same passports | ## When it does not do what you expect | Symptom | Cause and fix | | --- | --- | | The game waits for every file at the start | `defaultPhase` is `first` and no scenes split the plan: move files to `background` with `asset_preload` or `rules` | | The second visit downloads again | the 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 only | its phase is `lazy`, or an `asset.preload.plan` tap cancelled it | | The loading bar never shows | the view must read `asset.preload.progress`; `hud.action` needs `loadingBar: true` | | `of(ref)` answers null | the asset has no passport; the kit must use its defaults |