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
| File | Stage | Holds |
|---|---|---|
data/<registry>/*.ron | declare | one entry, or a list of entries, each a record with id: "<mod-id>:<name>" in the mod’s own namespace |
data/<registry>/*.update.ron | update | a list of Patch(..) and Declare(Entry(..)) items |
data/<registry>/*.final.ron | final-fixes | FinalFixes(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
- settings: startup values (
DataStage::set_setting) readable by derive steps andresolve. - 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.
- 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. - derive: engine steps (
DataStage::add_deriver) over every entry, after all user stages. - 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. - freeze: dense ids in sorted key order, so server and client agree whatever the load order.
- 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.
| Registry | Side | Source | Entries |
|---|---|---|---|
manifold:component | Both | code | every component in ComponentMetaRegistry (layout, codec, policy) |
manifold:relationship | Both | data | the ECS core’s kinds (ChildOf, OwnedBy, Targets) and data-declared kinds (their components land in A2) |
manifold:spatial-layer | Both | data | the 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-table | Both | assets | the host’s loaded binding tables; dense ids equal BindingLibrary::dense_id |
manifold:archetype | Both | data | archetypes: 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) | data | the gameplay framework’s registries, with the engine’s entries as patchable code trees |
manifold:physical-material | Both | data | physical 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:style | Server | data | brains, 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:spawner | Server | data | spawn 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:
| Reference | Context | Check | Unknown key |
|---|---|---|---|
an ability’s input | DeclaredActions: every input action (EngineBuilder::declared_input) | check_simulation_action: a declared Simulation action of the kind asked for | DATA010; a Local or wrong-kind action DATA013 |
a block argument (manifold:surface_block’s block, ArgKind::Block) or a locomotion profile’s surface_malus mask | DeclaredBlocks: the world’s blocks (EngineBuilder::declare_world_blocks) | check_block | DATA010 |
an item kind’s explicit name | MessageCatalog: each mod’s default-locale message ids | check_message_key: a message key (DATA009 if not), defined in its mod’s catalog | DATA010 |
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.