Natural spawn rules are the type: spawn entries of Effects/: mobs appearing where the world suits them, rather than at a placed spawner. This is the surface a world generator wants.
The API
explain is the debugging entry point: it returns the same lines /nme spawnrule test prints, so you can show a player why a rule did or didn’t apply at a location.
Integrating a custom world generator
A plugin that invents its own biomes knows better than any config file which of them should have which mobs. Two things make that work, and neither needs changes on our side.
1. Custom biomes are matchable by their own namespace
isBiome{} compares the full namespaced key, so a generator’s biome is distinguishable from a vanilla one of the same name:
A bare name matches any namespace, so existing configs are unaffected. The same rule applies to isBlock, isBlockBelow and spawner conditions:.
2. Anything the key can’t express, register as a condition
If your world model has concepts that aren’t biomes — regions, dimensions, noise layers — registerCondition puts them in the same vocabulary, usable in every rule’s cond: and in ordinary if lines:
Use ctx.getLocation(), not ctx.getMob(). A spawn rule evaluates conditions before anything is spawned, so there is no caster and getMob() throws. getMobOrNull() is there for conditions that work either way.
Shipping bundled rules
Contributed rules merge into the registry and survive /nme reload.
A server owner overrides any of them by declaring the same id in an Effects/ file: the generator supplies sensible defaults, the owner has the last word. Calling contribute again replaces the whole contributed set.
Constructor shapes
SpawnRule takes (id, mob, condition, rate, caps, pack, replaces, suppressOriginal, level, effects, enabled, source) and is @JvmOverloads, so you may stop after any prefix — new SpawnRule("id", "mob") is valid. A condition may be a compiled cond: string or, as above, your own lambda.
SpawnCaps(perChunk, perWorld, perPlayer) is @JvmOverloads too. 0 means no ceiling.