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:name works 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:

StyleWhere it appearsExample
snake_casemost discriminators & enum valuestype: builtin_simplex_noise, frame: first, one_shot
lower-case wordshort categoriescategory: sfx, crafting-station.type: crafting
SCREAMING_SNAKE_CASEcontrol actions & categoriesaction: MOVE_EAST, category: STRUCTURE_EDITOR
PascalCase code nameskeyboard 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_launcher and type: projectile-launcher are the same value, as are formation: trail-chain and formation: 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 content name — where my-thing and my_thing are two different names, nor to a control binding’s key, which is exact-match (KeyD must 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:

KindFieldsAn unrecognized value
Closed set — fixed by the enginean 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 typeFails the entry at load, naming the value
Open registry — you define the tagtool-types / tool-break-types, a crafting station’s type, an item’s material-types, a mob’s spawn-group, ammo-type, default-stateIs 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/HOSTILE are one group — see Mob Configuration), material-types, a block-shape’s type/side, a block’s facing, a weapon-info’s type, tool categories, armour slots, item types, an entity type, control actions/triggers/modifiers, a sound’s category, a structure’s air-blocks/surface-snap/spawn-type, a mob body: block’s formation/damage-model, an AoE shape/damage-mode type, a command’s permission, a terrain-generator’s and sky-island shape-generator’s type, a shader’s type, a mod’s side, the world-type selector, and the texture-state frame: first/last keyword. 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:

FormExamples
bare adjectivecollidable, breakable, stackable, flying, enabled, preload
verb phrasedrops-self, falls-when-unsupported, takes-damage, check-spawn-clearance
modal / requirementmust-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 / formUnitExamples
-msmillisecondstime-to-break-ms, hit-cooldown-ms, frame-time-ms, telegraph-duration-ms
-per-secondrate, per real-time secondregen-per-second, consumption-rate-per-second, fluid-flow-per-second
-secondswhole secondsday-length-seconds, auto-save-interval-seconds
_s (inside expressions)secondsanimation_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:

SuffixUnitWhere
-pxpixelsan icon’s tool-options: {x-offset-px, y-offset-px}
-degreesdegreestexture-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:

FieldWhereRead by
max-speeda standalone mount, or a mob’s mount: blockyour server-update hook, via entity:get_max_speed()
jump-strengthsameyour 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 familyUsed byMeaning
min-altitude / max-altitudeterrain generators, ore, sky-island features, underground features, biome hard bounds, flat-world bands, particle emittersworld-Y band
max-altitude (single bound)world-builder poolspools form at or below this world-Y
min-thickness / max-thicknessbiome ground layersthickness below the surface, not world-Y

Range ordering is uniform: every min-*/max-* band is ascending (min ≤ max) in world-Y, so min-altitude is always the lower (deeper) bound and max-altitude the 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 single max-altitude, which means “form at or below this world-Y” rather than one end of a range.

Only thickness is not world-Y. A biome’s ground layers measure min-thickness/max-thickness downward from the surface, so they’re the one vertical field that isn’t an absolute altitude. Everything else on the vertical axis is altitude.

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:

FormUsed byMeaning
amount: Ncrafting outputs & ingredients; drop tables; loot tablesa fixed quantity
min-amount / max-amountblock, mob, mount, tree drop tables; loot tablesa 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 in 0.01.0. Each entry rolls on its own. Used by block/tree/mob drops, particles, and structure spawns (spawn-chance, and the per-link chance inside an any-of: group).

When the roll happens differs, even though the number means the same thing. A drop or particle chance is rolled at runtime, freshly, every time the event occurs — break the same block twice and you get two independent rolls. A worldgen spawn-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.7 fills 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 by spacing, not the block, so halving spacing roughly quadruples how many objects a given spawn-chance yields.

  • weight — a relative likelihood among a group of options (default 1.0; 0 disables). The engine picks one entry in proportion to its weight against the group’s total. Used by loot-table entries, drop-table groups, tree variations, a biome’s weather-options, and a structure link’s one-of: group.

The clearest place to see the difference is a structure link, which takes whichever of the two you ask for. A links group written any-of: gives each candidate its own chance and rolls them independently, so zero, one, or several attach. The same group written one-of: gives each candidate a weight and 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 abundance is a calibrated fraction (0.01.0) of how much of the eligible band fills with the ore — its own thing, neither chance nor weight.

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.01.0:

color: {r: 1.0, g: 0.6, b: 0.2, a: 1.0}

There are no hex strings, no 0255 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.yaml starts with items:, block-config.yaml with blocks:, and likewise mobs:, 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.