Levels and packages
A level is a folder (or a .ferret package) with a glTF scene and a manifest that says which nodes are gameplay entities, where the player may start and how the level looks and behaves. Artists move things in Unreal or in the editor; because the manifest refers to nodes by name, the wiring survives a re-export.
The manifest#
manifest.json (LevelManifest) sits next to scene.glb:
{
"version": 1,
"name": "Yard",
"scene": "scene.glb",
"navmesh": "navmesh.bin",
"defaultSpawn": "gate",
"spawns": [{ "id": "gate", "position": [0, 0, 8], "yaw": 3.14 }],
"entities": [
{ "id": "door1", "node": "Door_Main", "behaviour": "Door", "params": { "slide": [0, 2.4, 0] } },
{ "id": "key", "node": "Key", "behaviour": "Pickup", "params": { "item": "brass-key" } }
],
"colliders": [{ "node": "Fence_Decor", "collide": false }],
"environment": { "sun": { "intensity": 3 }, "fog": { "density": 0.01 } },
"physics": { "capabilities": ["gravity", "friction"], "gravity": { "vector": [0, -9.81, 0] } }
}
| Field | Meaning |
|---|---|
scene, navmesh, collision | file names; a collision mesh of simple shapes replaces the visible meshes as solids |
collisionChunks | the solid shapes outside the play area in chunks (loadRadius, unloadRadius, chunks: a file and its bounds each); a chunk is fetched when the player is within loadRadius of its bounds and released beyond unloadRadius; needs collision |
navBuild | how to build the NavMesh when navmesh.bin is missing: agent size, climb, slope, cell size; bounds (one {min, max} box in world metres, or a list: the NavMesh is built inside the union, like several Unreal NavMeshBoundsVolumes); exclude (boxes where nothing is walkable, like NavModifierVolumes with the null area; yaw, radians about +y, turns a box about its centre) |
entities | ManifestEntity: a node to wrap (or a new empty one), a behaviour or several components, params, tags, a transform |
spawns, defaultSpawn | where the player may start; the level is entered at defaultSpawn when none is named |
zones | named zones for the ZoneService |
movement | the player's movement values for this level, over Ferret's defaults |
animGraphs | animation graphs by name |
colliders | per-node overrides: make a node solid or walk-through whatever the export says |
environment | sun, ambient light, fog, sky, fixed effects (EnvironmentDef) |
physics | realism for this level: materials, pair rules, capabilities and their values (Physics overview) |
validateManifest returns a list of problems (path and message); an empty list means the manifest is usable. The same check runs at load time and in the editor.
The level service#
LevelService is the level registry and travel. A game registers LevelDefs, each with a builder that gets a LevelContext. travel(id) shows the loading screen, removes the old level, builds the new one, places the player at a spawn and hides the loading screen again; it reports with events (level:loading, level:loaded, ...). Everything a builder creates (nodes, lights, materials, entities) is removed again when the level goes, so levels do not leak into each other.
A LevelDoor in the level is the usual way into another one: using it calls travel.
Packages#
installPackages teaches a LevelService to play packages: a folder with a manifest, or a single .ferret file. PackageMount registers the package's levels (the first package's levels as <id>, later ones as <pkgid>/<id>). Packages are loaded from a URL, from a File, or from bytes; the pkg.* commands add and remove them at run time (Commands).
A package can be signed and its files encrypted; the loader checks signatures, hashes and paths before anything is used, and a package that carries code runs under a CodePolicy. validatePackage runs the package's own checks. See @ferret/assets for the format and its security checks.
Code in a package#
A package of kind library (or a game) may carry code: one bundled ES module under code/, named in ferret.json as "code": { "entry": "code/index.js" } (a game may name it as entry.code), that exports install(ctx). installCode gives a mount a CodeLoader; require(id) (or the command pkg.require { id }) runs a package's code:
- the code of its dependencies first (each must be open, or opened through the loader's
openhook, e.g. from the catalog, and fit its version range); - the package must pass its
CodePolicy: it comes from where the game does (origin), or it carries a valid signature (from a trusted key when the game keeps a list); - the module is read once and checked against its sha256 in the manifest, and those bytes are imported (a blob URL), so what runs is what was checked;
install(ctx)is called once.ctxhas theworld, the engine's API asctx.ferret, the package (ctx.pkg),ctx.url(path)for its files andctx.require(dep)for a dependency's exports. It may return{ dispose }.
The module does not import the engine: it is built with @ferret/* and Babylon as externals and reaches them through ctx.ferret and the global BABYLON, so it runs against the shared engine (lib/ferret/ferret.js, globalThis.Ferret). A game that bundles its own copy of the engine passes its API as the loader's ferret, or library code is refused with that reason. A page runs one version of a package's code: a module cannot be unloaded, so another version needs a reload. pkg.loaded lists what runs; code:loaded and code:failed are the events. provides names what a library's code registers (capabilities, kinds, flowSources, forms, commands, levelKeys), so a level can be resolved to the packages it needs without running them.
Environment#
applyEnvironment builds a level's look in the scene (sun, ambient light, fog, sky, effects) and returns an Environment to dispose when the level goes. Quality tiers decide which effects are on.