Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Data stage and registries

Every registry of game definitions (archetypes, relationship kinds, spatial layers, components, and Talos B–E’s stats, tags, effects, brains and the rest) goes through one data stage before the world ticks. Mods declare and patch entries by namespaced key in load order; the stage validates every reference with a file, line and column, freezes each registry into dense ids, and hashes the result into the connection handshake. The design and its rationale are in Talos A (docs/superpowers/specs/2026-09-27-talos-a-entity-core-design.md) §12; this page is the current contract.

Source: manifold_engine::data (crates/manifold-engine/src/data/) (the stage, trees, registries, hashing), the boot stage every host runs in manifold_server_state::data_stage (crates/manifold-server-state/src/data_stage.rs), and read_data in manifold-mod-archive (crates/manifold-mod-archive/src/lib.rs). Tests: crates/manifold-engine/src/data/tests.rs, crates/manifold-engine/tests/data_hash_golden.rs, and the data_stage tests in manifold-server-state and tools/cargo-mod, and the generated reference’s data_reference tests in manifold-server-state.

Files

A mod’s data lives under data/ in its package, one directory per registry, next to mod.toml. The mod declares the format once, in [package]:

[package]
id = "petmod"
# …
data_format = 1
FileStageHolds
data/<registry>/*.rondeclareone entry, or a list of entries, each a record with id: "<mod-id>:<name>" in the mod’s own namespace
data/<registry>/*.update.ronupdatea list of Patch(..) and Declare(Entry(..)) items
data/<registry>/*.final.ronfinal-fixesFinalFixes(reason: "…", patches: [ … ]); the reason is required and recorded

Each registry a mod can author owns one directory. The data registry reference lists every directory, with each entry’s fields, types, defaults and an example; it is generated from the code. Components (manifold:component) come from component metadata and binding tables (manifold:binding-table) from the model assets; neither is declared in data. A file in an unknown directory is an error (“did you mean”).

// petmod: data/relationships/tamed_by.ron
Relationship(id: "petmod:tamed-by", exclusive: true, on_delete_target: Remove,
             on_unload_target: Keep, persist: true, replicate: Public)

// petmod: data/spatial-layers/burrow.ron
SpatialLayer(id: "petmod:burrow")

// otherpet: data/relationships/tamed_by.update.ron
[
    Patch(target: "petmod:tamed-by", ops: [Set(path: ["persist"], value: false)]),
    Patch(target: "wolves:pack", optional: true, ops: [Remove(path: ["replicate"])]),
]

cargo mod build packs data/ into the .mod; the browser delivery artifact carries it too. The flagship’s bundle packs each baked module’s data/ (game/flagship-game/data/) into that module’s asset zip.

Stages and order

  1. settings: startup values (DataStage::set_setting) readable by derive steps and resolve.
  2. declare: entries declared in code first (the engine’s, then each mod’s before its files), then every mod’s declaration files, mods in load order, files in path order.
  3. update, then final-fixes: every mod’s patch files, in the same order. A patch whose target does not exist fails with its file and line unless it says optional: true.
  4. derive: engine steps (DataStage::add_deriver) over every entry, after all user stages.
  5. validate: each registry, in dependency order, turns each entry’s tree into its typed value (RegistryEntry::resolve) and runs its registry-wide rules. Every error is reported, not the first.
  6. freeze: dense ids in sorted key order, so server and client agree whatever the load order.
  7. hash: see below.

Patch operations are Set (replace or add a field), Merge (records by field, maps by key, recursively; lists and scalars replace), Append (a list value appends its items) and Remove. Paths are the asset editor’s AssetFieldOp.path form: names from the entry’s root, name[i] for list element i; map keys that contain : or . need no quoting. Some(x) and other one-item wrappers are transparent to paths.

Two mods’ patches that set the same value are legal; the later in load order wins and the stage reports the overlap once, as a warning.

Provenance and diagnostics

Every node of every entry records every write to it: mod, file, line, column, stage and (for final-fixes) the reason. After freeze the records move to a side table (DataRegistries::provenance), so frozen entries carry none.

Diagnostics name the value’s location and cite earlier writes:

error[data]: "base:cc" is not a test:stat entry
  --> base:data/creatures/x.ron:2:5 (test:creature base:x, health)
   = help: did you mean "base:c"?

They are developer text. A required mod with errors fails startup; the low-level stage can disable an optional mod with its error list and rerun without it. Production boot treats every active game module’s data as required: invalid data fails boot or join, because dropping its data while running its code would leave a partial module. Codes are stable (DATA001…DATA017, DiagCode).

Each ModData carries an origin: ModOrigin::Bundled (the default from ModData::new: shipped with the game, or a test), Installed (a local .mod) or Delivered (sent by a server at join), set with ModData::with_origin. It is for diagnostics only. A diagnostic in a listed mod names it (DataLocation::origin, printed after the location as [installed], and an origin field in the JSON path), and so does DisabledMod::origin and its DATA015 warning (“optional installed mod … was disabled”). The stage behaves the same for every origin, and the data hash never includes it (data_hash_golden pins one hash for all three).

cargo mod validate <mod.toml> [--base <mod>]… [--json] runs the same boot stage offline, with the base mods loaded first. --json adds a data section to the schema-1 validation document, beside models and graphs, with the same record shape (severity, code, path, message, repair); the path carries the mod, file, line, column, entry and field.

The validator checks data against what each mod’s code declares, folded as the hosts fold it: it reads a game module’s ModDeclarations from its built component under the declarations profile (as cargo mod build does) and takes the guest-declared novel components, ModApp archetypes, input actions and blocks from its server manifests, with the engine’s baseline actions and every mod’s default-locale catalogs. A base given as a directory needs its component built there (cargo mod build, or the manifest’s wasm); a .mod carries it. A game module with Rust code and no built component is an error that says so, rather than a list of its components reported as unregistered. A mod without code declares nothing.

cargo mod build --manifest game/flagship-game/mod.toml
cargo mod validate meadowmod/mod.toml --base game/flagship-game

Registries

A registry is a type implementing RegistryEntry: its name (manifold:archetype), its side (Server or Both), its source (Data { dir }, Code or Assets), the registries whose resolved entries it reads, a resolve from its tree, and the canonical bytes hashed into the data hash. Register it with DataStage::register in boot_stage (crates/manifold-server-state/src/data_stage.rs), where every host and cargo mod validate build their stage. Read it after boot with engine.data().get::<T>() (Registry<T>, ids RegistryId<T>), or from the world resource Arc<DataRegistries>.

Inside resolve, ResolveCx::decode reads a typed value from a tree with serde and reports failures at the innermost node; ResolveCx::resolve_ref turns a key into an EntryRef<T> (key plus dense id) and reports unknown keys with a suggestion. Canonical forms write references as keys.

Documenting a registry. A registry a mod can author also gives RegistryEntry::DOC, a paragraph on what it is and when to use it, and RegistryEntry::file_schema, a #[derive(Schema)] description of its entry format (by convention in its crate’s file_schema.rs) whose /// docs are the reference’s text; an optional field’s doc ends with a Default: line. resolve stays the decoding truth: the crate’s tests compare the schema with the serde types and known-field lists resolve reads (data::serde_matches), and manifold-server-state’s data_reference tests check every schema against every bundled data file and the engine’s code trees, require a doc on every type, field and variant, and keep the generated reference current (MANIFOLD_REGEN_REFERENCE=1 rewrites it). An engine-internal or test-only registry sets RegistryEntry::INTERNAL instead; that is the only exclusion.

RegistrySideSourceEntries
manifold:componentBothcodeevery component in ComponentMetaRegistry (layout, codec, policy)
manifold:relationshipBothdatathe ECS core’s kinds (ChildOf, OwnedBy, Targets) and data-declared kinds (their components land in A2)
manifold:spatial-layerBothdatathe ECS core’s built-in layers (player, creature, item, projectile, prop) and mod layers, 32 at most; spatial_layer_registry gives the index’s LayerMask bits
manifold:binding-tableBothassetsthe host’s loaded binding tables; dense ids equal BindingLibrary::dense_id
manifold:archetypeBothdataarchetypes: inheritance by a derive step, each variant’s encoded components; ModApp::add_archetype* declarations as code trees a same-key data entry of the same mod supersedes
manifold:tag, manifold:stat, manifold:effect, manifold:damage-type, …Both (manifold:loot-table: Server)datathe gameplay framework’s registries, with the engine’s entries as patchable code trees
manifold:physical-materialBothdataphysical materials (docs/architecture/physics-runtime.md): density, friction, restitution and impact profile; the engine’s nine as patchable code trees
manifold-ai:brain, manifold-ai:state-tree, manifold-ai:memory-kind, manifold-ai:awareness-profile, manifold-ai:lod-profile, manifold-ai:curve, manifold-ai:styleServerdatabrains, state trees, memory kinds, awareness and LOD profiles, response curves, styles; brain inheritance by a derive step
manifold-ai:spawn-rule, manifold-ai:spawn-category, manifold-ai:spawnerServerdataspawn rules, categories and content-placed spawners

References to code declarations

Some references name what code declares rather than a registry entry. The host supplies these as stage context (BootDeclarations, built from the engine under construction by BootDeclarations::from_builder), and a resolve checks them with a ResolveCx method:

ReferenceContextCheckUnknown key
an ability’s inputDeclaredActions: every input action (EngineBuilder::declared_input)check_simulation_action: a declared Simulation action of the kind asked forDATA010; a Local or wrong-kind action DATA013
a block argument (manifold:surface_block’s block, ArgKind::Block) or a locomotion profile’s surface_malus maskDeclaredBlocks: the world’s blocks (EngineBuilder::declare_world_blocks)check_blockDATA010
an item kind’s explicit nameMessageCatalog: each mod’s default-locale message idscheck_message_key: a message key (DATA009 if not), defined in its mod’s catalogDATA010

Each suggests the closest key. A context the host does not supply is not checked, as for an asset registry it does not load. The flagship hosts supply actions and blocks; they load no catalogs on the server, so item names are checked as keys only there, and fully by cargo mod validate. References to other registries resolve as EntryRefs: a loot Item’s item kind, a brain’s ChargeAttack and UseAbility ability (D checks it while the brain compiles, through ResolveCx::holds).

Sides and the handshake

A server (and a single-player host) loads every registry; a client skips Server registries. A Both entry may name a Server registry only inside ResolveCx::server_only (a ServerOnly component’s value); a client resolves those to no id, and canonical forms leave them out.

The data hash is SHA-256 over each Both registry (asset registries excepted) in name order: its name and each entry’s key and canonical bytes in id order. WorldInfo.data_registry_hash carries it with the per-registry digests (protocol 45); the client checks it after the archetype hash and a mismatch names the first differing registry. Players see the message key handshake-data_registry_mismatch with the registry as its argument; the digests are developer diagnostics. Server registries are hashed separately (DataRegistries::server_hash).

Every host builds its engine with install_boot_mods over one module list (data_stage::boot_mods, Enki B §10.3 and issue 40): the data/ tree of every module of the session’s module set, in its load order (dependencies first, ties by mod id), each read from the module’s archive with its origin: Bundled for a baked module, Installed for one from the host’s mods directory (a loose directory, packed when the set is resolved, or a .mod), Delivered for one a server sent. The set holds one source per id, so the list does too. The built-in and novel components and the binding tables the model catalog loads join it, and data that names a block is checked against the world’s blocks (EngineBuilder::declare_world_blocks, Talos A §12.5). Duplicate ids and invalid dependencies fail boot. Client plugins and translation packs do not contribute to the world data hash. A content-hash mismatch also names the game modules whose digests differ (WorldInfo.game_module_digests, protocol 54).

A host that runs a world (a dedicated server, mp-host, native singleplayer and hosting) stages its local set: its baked modules and the game modules installed in its mods directory. A client joining another server stages its baked modules only; the player’s installed game modules never join another server’s world.

Clients retain that list. On join, they read and verify the requested delivery archives (including warm-cache archives and installed .mod files whose bytes are the server’s artifact) and resolve the session’s set, each delivered module (origin Delivered) replacing any source of its id. A joining client builds its session content from that set before comparing any hash (Enki B issue 40, phase 3; mod contract): the data stage runs inside the session engine’s build, after the fold and the block registry and with the session’s model binding tables, exactly as at boot, and the connect compares WorldInfo.data_registry_hash with that engine’s registries. Engine::data_with_delivered (#521) is the data-only form of the same rebuild, kept for a connect without a session content builder. Either way the resulting registries and game-rule declarations belong to that connection’s world, so a later connection starts from the original list. Clients run the stage themselves; the server never ships frozen registries.

Limits

  • Hot reload of definitions (§13) is Talos A2.
  • Asset registries are outside the data hash because clients load assets after connecting; references into them hash by key.