Game rules
Game rules are typed settings that change which rules of the game run in a world: whether the sun moves, which destruction preset applies, how many creatures may wander. The engine owns them. A rule is declared once, with its type and default; a world then sets values for the whole world and, where it wants, per world space. Values persist with the world and replicate to every client.
Source: manifold_engine::world::game_rules (crates/manifold-engine/src/world/game_rules/)
(declarations, scopes, validation), persistence and the guest mirror in
manifold_server_state::game_rules (crates/manifold-server-state/src/game_rules.rs),
the save table in
manifold_saves_types::game_rules (crates/manifold-saves-types/src/game_rules.rs),
the boot load in manifold_host_core::boot::seed_game_rules, and /gamerule
in game_rule_commands (crates/manifold-server-state/src/console/game_rule_commands.rs).
Declaring a rule
Rules live in the manifold:game-rule registry of the
data stage, so a mod declares them in data/game-rules/ (or
in code with DataStage::declare_code_entry), and another mod may patch a
default with an .update.ron like any other entry. The registry is loaded by
both sides and covered by the handshake’s data hash.
// example: data/game-rules/max_wanderers.ron
(id: "example:max-wanderers", kind: Int(min: 0, max: 64), default: 8)
kind | Value | Checked |
|---|---|---|
Bool | true/false | type |
Int, Int(min:, max:) | integer | range |
Float, Float(min:, max:) | number | finite, range |
String, String(max_len:) | text | length in bytes (default 256, at most 4096) |
Key(registry: "<mod>:<registry>") | a key of that registry, such as a preset entry | key exists (a Both rule may not name a Server registry) |
Enum(["a", "b"]) | one of the names | membership |
Key and Enum rules hold RuleValue::Key. The engine declares
manifold:do-daylight-cycle (Bool, default true) in code; a host that runs
no data stage has only the engine’s rules (GameRuleDecls::builtin). Rule keys
are ordinary registry keys (<mod-id>:<name>), so two mods cannot collide.
Scopes and resolution
GameRules holds explicit values at two scopes (RuleScope): World, which
every space inherits, and Space(WorldSpaceId). A space’s value is its own
override, else the world value, else the declared default; get_* resolves,
explicit does not. Defaults are never stored, so a changed default reaches
every world that never set the rule.
Every write goes through GameRules::set, which checks the value against the
declaration and returns a RuleError naming the rule (unknown key with the
closest declared key, wrong type, out of range, not in the registry or enum,
too long); nothing changes on error. GameRuleDecls::parse reads console text
by the rule’s type. reset clears an explicit value.
A saved value for a rule no declaration names (a mod that was removed) is kept and saved again, but never resolved, so re-enabling the mod restores it. A saved value that no longer fits its declaration is dropped with a warning when the world loads.
Persistence
The world’s rules load at boot, before run_on_server_world_ready and the first
tick, on both hosts (seed_game_rules over the SaveStore seam), so tick 1
already runs under the saved rules. Every save writes them in the same
transaction as chunks, players and metadata.
Table game_rules_v1 | |
|---|---|
| Key | scope: [0] for the world, [1] + the 16 bytes of the WorldSpaceId for a space |
| Value | postcard GameRuleScopeRow { schema_version: 1, rules: Vec<(key, PersistedRuleValue)> }, keys sorted; PersistedRuleValue is Bool, Int, Float, String, Key in that tag order |
A flush replaces the whole table; a scope with no explicit value has no row. A
row with an unknown scope key, another schema version or undecodable bytes is
skipped with a warning. The table is additive, so the world format version is
unchanged. A world saved before the table has its rules in the legacy
WorldMetaDynamic.game_rules_ron blob: when the table has no rows, the loader
imports that blob into the world scope (doDaylightCycle becomes
manifold:do-daylight-cycle; values equal to the default and names with no
declared rule are dropped), and a flush that writes the table writes the blob
as None.
Replication and mods
GameRules is an engine-baseline synced resource (manifold:GameRules), sent
in the join snapshot and on change. The payload is postcard of the explicit
values (StoredGameRules) for every scope; each client rebuilds GameRules
over its own declarations, which the data hash guarantees match the server’s.
Every client receives every space’s values: rule sets are small, public server
configuration. Anything that changes rules calls
mark_synced_dirty::<GameRules>().
Guests read the read-only manifold:game_rules mirror
(manifold_server_api::GameRulesBridge): every declared rule’s world value and
each space’s overrides, with get(space, key) resolving an override. Guests do
not write it; rules change through the server’s permission-checked command
surface, which reports a rejected value to its caller.
Console
/gamerule runs on the server (both hosts register it):
| Form | What |
|---|---|
/gamerule | every rule’s world value and whether it is set or the default |
/gamerule <rule> | the world value, the type, the default and each space’s override |
/gamerule <rule> <value> [<space>] | set the world value, or a space’s override (Admin) |
/gamerule reset <rule> [<space>] | clear a value (Admin) |
<rule> is the key, or its name after the colon when only one rule has it.
<space> is here (the caller’s space) or a hex space id (0 is the default
space).
Tests
cargo test -p manifold-engine --lib game_rulesandbaseline_synced: resolution across scopes, validation of every kind, console parsing, data declarations and their diagnostics, undeclared and invalid saved values, the per-space sync payload onto a client world.cargo test -p manifold-saves-types --features redb --lib game_rules: the table’s rows, replacement and skipped rows.cargo test -p manifold-server-state --lib game_rulesandcargo test -p manifold-host-core --lib boot: save round trip, the legacy import, a world with no rules, the boot seed and/gamerule.cargo test -p manifold-server --test suite persistence_e2e::game_rules_survive_restart: a real server loads world and space values and an undeclared value at boot and writes them back on shutdown;vanilla_e2eshows the flagship clock following the world rule and ignoring a space override.