Mod-owned terrain recipes
The shipping terrain path is a server declaration in the WASM game manifest. Flagship uses the same declaration and host loader available to other mods. The engine owns the evaluator, validation, exact/coarse generation, caching and persistence; the mod owns its recipe, biomes, material names and default choice.
Authoring
In the guest’s Plugin::build:
app.add_terrain_recipe(
"overworld",
include_str!("../assets/terrain/world.ron"),
Some(include_str!("../assets/terrain/baked.ron")),
true,
);
The arguments are a local provider id, procedural recipe RON, optional baked heightfield recipe RON, and whether it is the default. A terrain-only plugin is valid. Declarations appear only in the server manifest. Manifest format 11 requires rebuilding existing WASM mods; older versions fail with a rebuild message rather than being decoded under a different layout.
The host prefixes the local id with the authenticated package id, regardless
of a guest’s claimed plugin namespace. For Flagship the key is
flagship-game:overworld. Duplicate keys fail. A nonempty catalog needs exactly
one default unless the host selects a key explicitly; multiple defaults fail
instead of choosing by load order.
Native hosts discover directory and .mod archive providers alongside bundled
mods, honoring directory overrides and package dependency/collision checks. Disk
providers use the ordinary game-module sandbox; manifest reads do not fire
world-ready hooks. Terrain preflight errors are fatal, and a selected provider
that is later disabled by the orchestrator prevents world startup.
Native hosts support the
MANIFOLD_TERRAIN_RECIPE=package:local-id override. Browser singleplayer selects
the catalog default. Empty catalogs are permitted for native engine harnesses;
they use the existing flat test world. A requested missing provider is an error.
The current evaluator is the versioned scripted Cartesian terrain implementation.
Recipes are limited to 1 MiB, four to 64 biomes, seven validated stage kinds,
bounded spawn radii/heights and feature spacing/chance. Material names resolve
against the final block registry and unknown names fail at boot. The v1 evaluator
uses biome slots 0–3 as lowland, shore, snow and exposed terrain for baked fields;
procedural trees use slot 0. These are evaluator semantics, not Flagship block
names. Use the game-owned recipes in game/flagship-game/assets/terrain/ as complete
examples. Semantic recipe/biome ids inside RON contribute to content hashing; they
do not grant registration authority in that namespace.
This contract installs data into the existing WorldGenerator path, including its
exact generation, certified coarse pages and semantic identities. It does not
execute arbitrary guest callbacks for each voxel. New bounded stage operators,
composable contributions and richer biome rules remain extensions of the terrain
design; do not bypass the shared evaluator or introduce a Flagship-only hook.
Saves and compatibility
Native and browser OPFS saves bind terrain_recipe_v1 in world_meta to UTF-8
<provider-key>\n<resolved-recipe-identity-hex>. The identity includes the seed,
recipe parameters and material names; baked worlds also include the artifact
digest. Reopening with changed content/provider fails before generation. Restore
the original mod or create a new world; do not delete this binding as a migration.
Flat native harness saves receive a distinct binding too.
A save without a provider binding adopts the installed recipe on first open; subsequent opens enforce it. That save may have been generated with different settings. Back up the world before changing its content set. Frozen fixtures verify procedural and baked recipe identities and generated output.
MANIFOLD_TERRAIN_BAKE imports an immutable field only into a new native world,
which persists it in terrain_bake_v1. Reopening uses those saved bytes and the
mod’s baked palette. Browser singleplayer reads the same seed, baked bytes and
recipe binding from its OPFS save; there is still no browser UI for importing a
new bake. Multiplayer browser clients consume the server’s streamed world.
Diagnostic tools take the recipe explicitly: lod-oracle --recipe FILE and
baked_probe <artifact> <blocks-dir> <recipe.ron> [--chunks repetitions].
Regression coverage
- A terrain-only guest emits its declaration through the ordinary manifest path.
- A non-Flagship provider installs and generates with a non-Flagship palette.
- Unknown materials, missing/default conflicts, duplicate ids and namespace spoofing fail; explicit selection is independent of declaration order.
- The compiled Flagship WASM guest is decoded by the real loader and compared with frozen recipe identities and generated chunks.
- Existing exact/coarse and baked terrain corpora remain compatibility checks.
- Save tests check idempotent binding, reopen and rejected recipe/provider changes.
Run the focused integration gates from the repository root:
cargo test -p flagship --test suite terrain_mod::
cargo test -p manifold-wasm-host-core --lib terrain_fold
cargo test -p manifold-mod-substrate --test terrain_recipe
cargo test -p manifold-saves --test suite roundtrip::terrain_recipe_binding
# After the normal browser build and e2e npm install:
node crates/manifold-host-web/e2e/assert-terrain-mod.mjs
Optional water networks
Water-enabled baked worlds attach their persisted water network through
TerrainCatalog::install_with_water_network. The selected mod still owns the
baked recipe and material palette; the water path does not substitute an
engine-owned recipe. A network requires a matching baked field, and the resolved
generator identity includes the network. Native save reopen validates the stored
network before binding the provider and recipe identity.