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, a block’s or item’s general tags | 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.
Tag pairs: what carries a tag vs. what requires one
Three of the open registries come in pairs — one key declares the tags a thing carries, another declares the tags something requires. Every one of these keys is plural, and every one accepts either a single value or a list, so there is one shape to learn rather than six:
| Concept | The key that carries the tags | The key that requires them |
|---|---|---|
| Material | an item’s material-types | a crafting ingredient’s material-types |
| Ammo | an ammo item’s ammo-types | a launcher’s ammo-types |
| Tool | an item’s tool-types | a block/interactable/tree’s tool-break-types |
Both halves of a pair are tag sets, so matching is an intersection: the requirement is satisfied when the two sides share at least one tag. An item carrying [PLANKS, WOOD] satisfies an ingredient accepting [PLANKS, LOG], because both list PLANKS.
# carries
material-types: [planks, wood]
# requires — satisfied by any item carrying PLANKS or LOG
material-types: [PLANKS, LOG]
Two of the three pairs even use the same key name on both sides; only the tool pair distinguishes them (tool-types vs tool-break-types), because a block’s requirement reads better as “what breaks this”.
This was not always uniform. A crafting ingredient once took a singular
material-typeand an ammo item a singularammo-type— two pairs with opposite conventions and no rule connecting a pair’s halves, so both spellings were a guess. Both are now plural and list-capable. If you have config written against the old spellings, rename the key; the singular forms are rejected as unknown fields, so nothing fails silently.
Values are matched case-insensitively (tags are normalized to uppercase), so planks and PLANKS are the same tag. The tag names themselves are an open registry — invent whatever you need — which is why a typo in a value can’t be caught the way a typo in a key is.
The general
tagsregistry is single-sided, not a pair. Separate from the three typed pairs above, blocks and items carry a general-purposetagslist — free-form membership labels with no built-in channel meaning. Unlike a pair, there is no matching “requires” key; instead atagsvalue is referenced from elsewhere with a#tagsigil (#ore), and the reference matches every block or item carrying that tag. It is global (never mod-prefixed), case-insensitive (normalized to uppercase,#stripped), and an open registry like the others — an unknown#tagis an empty group, reported where it’s used rather than at declaration. Block-side references are a block texture’stiles-withand a mob’svalid-spawn-blocks; the item-side reference is a drop-table entry’sitem: "#tag". See Block Configuration → Tags.
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.
Tool power vs. crafting tier
Two unrelated “how strong is it” axes sit near each other on the same interactable and are easy to conflate. Which one you’re looking at is determined entirely by where the key sits:
| Axis | Declared by the thing that has it | Declared by the thing that needs it | What it gates |
|---|---|---|---|
| Tool power | an item’s tool-info.power-level | a block / interactable / tree’s power-required | whether the tool can break it at all |
| Crafting tier | an interactable’s crafting-station.tier | a recipe’s crafting-station.tier | whether the recipe is craftable at that station |
They never compare against each other — a pickaxe’s power-level has no relationship to a furnace’s crafting tier. Tool power is a plain integer (higher is stronger, compared “has ≥ needs”; a bare hand counts as 1). The crafting tier is a named tier from a type’s crafting-types list, compared by list position (no exposed number).
This is easy to trip over because a single interactable carries both, a few lines apart:
- name: furnace
power-required: 2 # tool power needed to BREAK the furnace
tool-break-types: pickaxe
crafting-station:
type: smelting
tier: furnace # crafting tier the furnace PROVIDES to recipes
When editing either, check which block it’s in: anything inside crafting-station: is the crafting axis, anything outside it is the tool axis.
Block layers
Three fields refer to the block layers (0 = background/walls, 1 = foreground, 2+ = overlay), and they are three different relationships rather than three spellings of one:
| Field | Where | Shape | Means |
|---|---|---|---|
block-layer | a block entry | one integer | which layer this block type occupies |
block-layers | biome ground-layers / fill-surface-blocks, underground features | list of {layer, block} | which block fills each layer |
cave-layers | world builders | list of integers | which layers caves carve through |
Layer indices are always plain integers. In a block-layers list, a layer you don’t name defaults to Creation:air, so a sparse list is normal and meaningful — writing only layer: 1 leaves the wall layer empty on purpose. Naming a layer the world doesn’t have, or naming the same layer twice, fails the entry.
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.
The engine-enforced speed ceiling always lives at movement.max-velocity — on a mob, on the player, and on a mount. Its default is {50, 50} for a mob and the player, and {100, 100} for a mount, which is deliberate: a mount’s max-speed defaults to 80, so a 50 ceiling would clamp it. A mount’s max-speed/jump-strength stay at the top level rather than inside movement:, mirroring where the same two keys sit inside a mob’s mount: block.
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.
Last updated