Schema Conventions
Cross-cutting conventions shared by every YAML config — casing, units, coordinates, quantities, chance vs. weight, colors, and document shape — and the places they vary.
Every config file on this site is its own reference page, but the files share a common vocabulary: the same idea (a duration, a chance, a colour, a vertical position) recurs across many of them. This page is the one place that vocabulary is written down — read it once and most fields in every other file explain themselves.
It also does something the per-file pages can’t: it flags where the convention is not uniform, so that a value you copied from one file and pasted into another doesn’t silently mean something different. Where a rule has exceptions, they’re called out explicitly rather than smoothed over.
Two conventions are important enough that they live on the documentation home page instead of here — read those first:
- Naming & namespacing — how
ModName:nameworks and when to write the prefix. - Unknown fields — a misspelled key fails the entry at load time (with a handful of leniently-parsed nested blocks noted as exceptions).
Casing
Keys are always kebab-case. Every field name in every config is lower-case words joined by hyphens: max-stack-size, time-to-break-ms, walk-speed, frame-durations-ms. There are no snake_case or camelCase keys anywhere. If you’re unsure how to spell a new key, hyphenate it.
Values are looser, and the shipped files are genuinely mixed. Enum-style values (a type, a category, a state name) appear in several styles depending on the field:
| Style | Where it appears | Example |
|---|---|---|
snake_case | most discriminators & enum values | type: builtin_simplex_noise, frame: first, one_shot |
| lower-case word | short categories | category: sfx, crafting-station.type: crafting |
SCREAMING_SNAKE_CASE | control actions & categories | action: MOVE_EAST, category: STRUCTURE_EDITOR |
PascalCase code names | keyboard keys (JS KeyboardEvent.code) | key: KeyD, key: Digit1, key: ArrowUp |
A crafting station’s type: crafting and a structure’s spawn-type: surface are lower-case, spawn-group: Neutral is capitalised, and material-types: LOG and category: STRUCTURE_EDITOR are upper-case. All of them load, and none is more correct than the others — there is no single casing to “match what ships”, so pick a form and stay consistent within your own mod.
A closed-set value resolves whatever casing and whichever word separator you write.
type: projectile_launcherandtype: projectile-launcherare the same value, as areformation: trail-chainandformation: trail_chain. You don’t have to remember which multi-word values the engine spells with_and which with-— a distinction that used to matter and no longer does.This applies to closed sets only (the table below). It deliberately does not apply to names you invent — an open-registry tag, a
spawn-group, a contentname— wheremy-thingandmy_thingare two different names, nor to a control binding’skey, which is exact-match (KeyDmust be spelled exactly, and a lone-is the literal minus key).
Closed sets vs. open registries
Casing never changes meaning, but whether a value has to already exist does — and the schema has both kinds, which look identical in YAML:
| Kind | Fields | An unrecognized value |
|---|---|---|
| Closed set — fixed by the engine | an item’s type, armor-type, a weapon’s type, a sound’s category, a control trigger / modifiers, a structure’s spawn-type, a generator’s type | Fails the entry at load, naming the value |
| Open registry — you define the tag | tool-types / tool-break-types, a crafting station’s type, an item’s material-types, a mob’s spawn-group, ammo-type, default-state | Is simply a new tag; nothing to fail |
If a field is a closed set, a mod cannot extend it from YAML — armor-type: cape is an error, not a fifth armour slot. If it’s an open registry, inventing type: alchemy for a crafting station, or tool-types: hammer for a new tool category, is exactly how you’re meant to add one.
The practical consequence is that a typo in a closed-set value behaves like a typo in a field name: loud, at startup, naming the offender. That is deliberate. The worst case is tool-break-types — a dropped value there leaves the block with no tool requirement, so pickaxee would silently turn a pickaxe-only block into one breakable by hand.
Enum values are matched case-insensitively — write whatever casing you like. Every enum-style value resolves regardless of capitalisation: a mob’s
spawn-group(hostile/Hostile/HOSTILEare one group — see Mob Configuration),material-types, ablock-shape’stype/side, a block’sfacing, aweapon-info’stype, tool categories, armour slots, itemtypes, an entitytype, controlactions/triggers/modifiers, a sound’scategory, a structure’sair-blocks/surface-snap/spawn-type, a mobbody:block’sformation/damage-model, an AoEshape/damage-modetype, a command’spermission, a terrain-generator’s and sky-islandshape-generator’stype, a shader’stype, a mod’sside, the world-typeselector, and the texture-stateframe: first/lastkeyword. The one thing casing never does is change meaning, so the only thing worth optimising for is consistency inside your own mod.
Booleans
Boolean fields are plain true/false, but there is no single prefix convention — don’t assume an is-/has-/can- pattern. Across the schema they take whichever form reads most naturally for the field:
| Form | Examples |
|---|---|
| bare adjective | collidable, breakable, stackable, flying, enabled, preload |
| verb phrase | drops-self, falls-when-unsupported, takes-damage, check-spawn-clearance |
| modal / requirement | must-spawn-on-ground, requires-supporting-blocks |
is- / has- prefix (rare) | is-static, has-lifetime |
Because the form isn’t predictable, take the exact key from the field’s own page rather than guessing. A misspelled boolean doesn’t quietly default — an unrecognised key fails the entry at load time (see Unknown fields), so a wrong guess surfaces immediately instead of silently leaving the flag off.
Units & measurement
There is no single time unit — the unit is encoded in the key’s suffix, and you must read it. A field is never “just a duration”; it is a -ms field or a -per-second field or a -seconds field, and the three are not interchangeable.
| Suffix / form | Unit | Examples |
|---|---|---|
-ms | milliseconds | time-to-break-ms, hit-cooldown-ms, frame-time-ms, telegraph-duration-ms |
-per-second | rate, per real-time second | regen-per-second, consumption-rate-per-second, fluid-flow-per-second |
-seconds | whole seconds | day-length-seconds, auto-save-interval-seconds |
_s (inside expressions) | seconds | animation_time_s, total_time_s |
Distances, sizes, positions, ranges, and radii are in blocks (1 block = 16 px), as floats: interact-range, max-range, hitbox: {width, height}, an AoE start-radius. A projectile’s max-lifetime-ms (milliseconds) sitting next to its max-range (blocks) is normal — check the suffix, not the neighbour.
Two suffixes opt out of blocks, and both say so in the key. Where a value is genuinely about the texture rather than the world, it carries its own unit:
| Suffix | Unit | Where |
|---|---|---|
-px | pixels | an icon’s tool-options: {x-offset-px, y-offset-px} |
-degrees | degrees | texture-rotation-offset-degrees, on a projectile’s and a wielded item’s texture block |
Both live in texture configs, never in gameplay configs — a pixel offset nudges a sprite inside its frame, which has no meaning in block coordinates. Anywhere without one of these suffixes, assume blocks.
Light levels are 0–255, where 15 is treated as “full” block light (max-block-light-level).
Engine-applied vs. script-driven values
Most numeric fields are engine-applied: you set them and the engine acts on them directly, with no Lua involved. A max-velocity clamp, a mob’s walk-speed, a block’s time-to-break-ms, a projectile’s gravity-multiplier — all take effect on their own.
A small number of fields are script-driven: the engine only stores the value and hands it back to a Lua hook, which decides what (if anything) to do with it. Nothing happens on its own — if the hook is absent, the field has no effect at all. In the current schema these are exactly the mount-steering fields:
| Field | Where | Read by |
|---|---|---|
max-speed | a standalone mount, or a mob’s mount: block | your server-update hook, via entity:get_max_speed() |
jump-strength | same | your server-update hook, via entity:get_jump_strength() |
The trap is that a script-driven field looks identical in YAML to an engine-applied one, so it’s easy to set max-speed and expect the mount to move — but a mount (or rideable mob) with no server-update steering script will not move at all, no matter what max-speed says. (The engine logs a load-time warning when it sees this: a steerable entity with the steering fields but no server-update hook.) Contrast max-velocity, which sits right next to max-speed and is engine-applied — it’s the ceiling the physics object can never exceed, script or no script. When a field is script-driven, its per-page reference says so explicitly; treat that note as load-bearing.
Coordinates & vertical position
Horizontal/2D offsets are ordinary block coordinates (x-offset, y-offset). The vertical axis (world-Y) is altitude everywhere it appears as an absolute position — one vocabulary across every generator:
| Field family | Used by | Meaning |
|---|---|---|
min-altitude / max-altitude | terrain generators, ore, sky-island features, underground features, biome hard bounds, flat-world bands, particle emitters | world-Y band |
max-altitude (single bound) | world-builder pools | pools form at or below this world-Y |
min-thickness / max-thickness | biome ground layers | thickness below the surface, not world-Y |
Range ordering is uniform: every
min-*/max-*band is ascending (min ≤ max) in world-Y, somin-altitudeis always the lower (deeper) bound andmax-altitudethe higher one — even underground, where both values are negative (min-altitude: -2000,max-altitude: -20). The one non-band field is a world-builder pool’s singlemax-altitude, which means “form at or below this world-Y” rather than one end of a range.
Only
thicknessis not world-Y. A biome’s ground layers measuremin-thickness/max-thicknessdownward from the surface, so they’re the one vertical field that isn’t an absolutealtitude. Everything else on the vertical axis isaltitude.
Sentinel defaults: a few worldgen fields default to the full 32-bit integer range (2147483647, -2147483648) to mean “no bound.” You rarely need to write these — omit the field to get the default.
2D vectors
A pair of numbers is always written as a keyed map, never a positional array. Which keys depends on whether it’s a size or a position/velocity:
# Velocities, positional offsets, and grid spacing use x / y.
velocity: {x: 0.0, y: 5.0}
max-velocity: {x: 50, y: 50}
offset-start: {x: 0.0, y: 0.0}
spacing: {x: 20, y: 20}
# Sizes use width / height.
hitbox: {width: 1.0, height: 1.0}
max-size: {width: 10, height: 15}
Individual axes may be omitted and default independently (velocity: {y: 5.0} leaves x at 0.0). A handful of standalone offsets are instead split into separate x-offset / y-offset keys (e.g. a mob seat) — same idea, two scalar fields rather than one map.
A min/max range is not a vector, and doesn’t use this shape. A particle emitter’s random offset range is four independent scalars — x-offset-min, x-offset-max, y-offset-min, y-offset-max — because each axis carries two bounds rather than one value, and they follow the min-*/max-* convention instead.
Quantities & ranges
“How many of an item” is expressed three ways across the drop / loot / crafting systems. They are not interchangeable — a config only accepts the form its page documents:
| Form | Used by | Meaning |
|---|---|---|
amount: N | crafting outputs & ingredients; drop tables; loot tables | a fixed quantity |
min-amount / max-amount | block, mob, mount, tree drop tables; loot tables | a random quantity in [min, max] |
A crafting recipe never rolls a range (outputs are deterministic), so it has no min-amount/max-amount. Drop tables and loot tables accept both forms — the amount shorthand and the min-amount/max-amount range (write amount: 1, or min-amount: 1, max-amount: 1, for a fixed count).
Chance vs. weight
Randomness is expressed with two distinct concepts, and they are easy to swap because both are often small decimals:
chance— an independent probability in0.0–1.0. Each entry rolls on its own. Used by block/tree/mob drops, particles, and structure spawns (spawn-chance, and the per-linkchanceinside anany-of:group).
When the roll happens differs, even though the number means the same thing. A drop or particle
chanceis rolled at runtime, freshly, every time the event occurs — break the same block twice and you get two independent rolls. A worldgenspawn-chance(trees, structures, sky-island features) is resolved once per placement cell, from the world seed, when the chunk generates. It is still a true probability —spawn-chance: 0.7fills 70% of eligible cells — but it never re-rolls: the same seed always produces the same layout, and regenerating the world will not give you a different result. The unit is the placement cell defined byspacing, not the block, so halvingspacingroughly quadruples how many objects a givenspawn-chanceyields.
weight— a relative likelihood among a group of options (default1.0;0disables). The engine picks one entry in proportion to its weight against the group’s total. Used by loot-table entries, drop-tablegroups, tree variations, a biome’sweather-options, and a structure link’sone-of:group.
The clearest place to see the difference is a structure link, which takes whichever of the two you ask for. A
linksgroup writtenany-of:gives each candidate its ownchanceand rolls them independently, so zero, one, or several attach. The same group writtenone-of:gives each candidate aweightand attaches exactly one — never none. Each key is rejected in the other’s group, so the wrapper you choose determines which of these two vocabularies applies. See Structure Configuration.
One field breaks the naming pattern and is worth memorising:
- An ore’s
abundanceis a calibrated fraction (0.0–1.0) of how much of the eligible band fills with the ore — its own thing, neitherchancenorweight.
Loot tables add min-rolls / max-rolls, which choose how many times the weighted table is sampled — orthogonal to the per-entry weights.
Colors
Colours are the one fully consistent value type. Every colour, everywhere (light tints, AoE outlines, particle colours), is an RGBA object with float channels in 0.0–1.0:
color: {r: 1.0, g: 0.6, b: 0.2, a: 1.0}
There are no hex strings, no 0–255 byte channels, and no named colours anywhere in the schema. (The 0–255 range appears only for light levels, which are not colours.)
Document shape
Config files come in two top-level shapes. Know which one you’re writing before you add a wrapper key:
- Catalog files wrap a plural list.
item-config.yamlstarts withitems:,block-config.yamlwithblocks:, and likewisemobs:,mounts:,ores:,trees:,recipes:,loot-tables:,structures:,weathers:. Each list holds many entries. - Singleton / descriptor files are a bare root object with no wrapper.
player-config.yaml, every biome file, world-type, world-builder, and the underground/sky-island feature files describe a single thing, so their fields sit at the top level directly.
A common mistake when copying from a catalog file is to wrap a singleton file’s fields in a plural key it doesn’t use. If a page’s example has no items:-style wrapper, don’t add one.
Where to go next
Each config’s own page is the authoritative reference for its fields, defaults, and any convention it bends. This page only maps the shared vocabulary; when a file departs from it, the departure is documented on that file’s page. Start from the documentation home page for the full list.
Where this page covers shared vocabulary, Shared Blocks covers shared structure — the handful of nested blocks (hitbox, light, drop-table, scripts, particles) that appear identically inside many different configs, documented once each.