41 KiB
23 - Loot, Entities, Spawners, Markers
Loot tables decide what appears inside generated chests and what custom mobs drop. Entities describe a mob and its gear. Spawners decide when and where those entities appear. Markers pin spawners to specific blocks inside placed objects. This page explains how each system fires at runtime, then documents every field.
Related: 05 - Concepts & Pack Layout.md, 11 - Dimensions.md, 12 - Regions.md, 13 - Biomes.md, 19 - Objects.md, 20 - Object Placement.md, 03 - Configuration.md, 10 - Studio & VSCode Schemas.md.
The mental model
Two independent pipelines share the loot table format.
Containers. When a chunk finishes generating, Iris walks the blocks it recorded and fills every storage chest it placed. It builds a list of loot tables for that exact block, then rolls each one and drops the results into the inventory. The list comes from up to four sources, in this order: the object placement that owns the block, then the dimension, region, surface biome, and cave biome the block sits in. Each source can add to the list, wipe it, or only contribute when nothing else did.
Ambient mobs. A background loop ticks each Iris world roughly twice a second. Each tick it measures how crowded the world is, and if there's room it picks a handful of loaded chunks and tries one spawn in each. A spawn attempt gathers every spawner the dimension, region, and surface biome list, throws out the ones whose time/weather/rate/crowding gates fail, pools their entries, picks exactly one, and places one to a few mobs.
Markers bolt the second pipeline onto the first: an object placement can tag specific blocks it places, and a marker definition attaches spawners to whatever carries that tag. That's how you get mobs that appear inside a specific ruin rather than anywhere in the biome.
Everything below is deterministic from the world seed and the block position, so the same chest at the same coordinates always contains the same items.
Walkthrough: put custom loot in a dungeon chest
The goal is a chest inside one placed object that rolls your table and ignores the pack's global tables. Prerequisites: a validating pack, an .iob object containing at least one chest, and a biome that places it.
1. Write the table. loot/tutorial/dungeon-cache.json:
{
"name": "Tutorial Dungeon Cache",
"rarity": 1,
"minPicked": 2,
"maxPicked": 4,
"maxTries": 24,
"loot": [
{ "type": "iron_ingot", "rarity": 1, "minAmount": 2, "maxAmount": 6 },
{ "type": "bread", "rarity": 1, "minAmount": 1, "maxAmount": 4 },
{ "type": "diamond", "rarity": 6, "minAmount": 1, "maxAmount": 2 },
{
"type": "iron_sword",
"rarity": 8,
"minDurability": 0.4,
"maxDurability": 0.9,
"displayName": "&7Rusted Blade",
"enchantments": [{ "enchantment": "sharpness", "minLevel": 1, "maxLevel": 2, "chance": 0.5 }]
}
]
}
maxTries needs headroom over maxPicked, because a try that lands on a rare entry and fails is spent. With four entries and rare items at 1-in-6 and 1-in-8, 24 tries reliably reaches 2-4 picks.
2. Bind it to the placement. In the biome that places your object, on the objects[] entry:
{
"place": ["tutorial/dungeon"],
"chance": 0.02,
"overrideGlobalLoot": true,
"loot": [
{ "name": "tutorial/dungeon-cache", "weight": 1 }
]
}
overrideGlobalLoot: true means chests inside this object use only this table. Drop it if you want the pack's dimension and region tables mixed in.
3. Verify. Open the pack in Studio, find a placement, and open the chest. Every chest in that object should hold 2-4 stacks drawn from the four entries, with diamonds and swords showing up in roughly one placement in six and one in eight. To check without hunting for a placement, stand on a block and run /iris studio loot — it previews the tables that would fill a chest at your feet and adds debug lore naming the source table and its combined chance. That command is Bukkit and Studio only.
4. If chests come up empty. Check that the block is a storage chest — Iris only fills chest-family containers, and only ever requests the STORAGE slot type, so FUEL/FURNACE/BLAST_FURNACE/SMOKER entries never land in a generated container. Check world.postLoadBlockUpdates is on in settings.json; the container fill runs as part of that post-load pass. Check that the loot table key resolves — a missing table logs a warning and contributes nothing. Double chests fill from one half only (the one with the lower X, then lower Z), and that half fills the combined inventory, so an empty-looking half is normal.
Walkthrough: make a custom mob spawn in one biome
The goal is a geared zombie that appears at night in one biome and drops a custom item. Prerequisites: a validating pack, a land biome with load key tutorial/meadow, and world.ambientEntitySpawningSystem: true (the default).
1. Drop table. loot/tutorial/zombie-drops.json:
{
"name": "Tutorial Zombie Drops",
"minPicked": 1,
"maxPicked": 1,
"maxTries": 4,
"loot": [
{ "type": "iron_nugget", "rarity": 1, "minAmount": 1, "maxAmount": 3 }
]
}
2. Entity. entities/tutorial/zombie.json:
{
"type": "minecraft:zombie",
"surface": "LAND",
"customName": "&cMeadow Stalker",
"helmet": { "type": "leather_helmet", "rarity": 3, "leatherColor": "#3B5323" },
"loot": { "tables": ["tutorial/zombie-drops"] }
}
The entity's loot replaces the mob's vanilla drop table outright. Only tables is read here — mode and multiplier on an entity are ignored.
3. Spawner. spawners/tutorial/night-zombies.json:
{
"group": "NORMAL",
"maxEntitiesPerChunk": 3,
"timeBlock": { "startHour": 20, "endHour": 5 },
"weather": "ANY",
"allowedLightLevels": { "min": 0, "max": 7 },
"maximumRate": { "amount": 4, "per": { "seconds": 30 } },
"spawns": [
{ "entity": "tutorial/zombie", "rarity": 1, "minSpawns": 1, "maxSpawns": 2 }
]
}
timeBlock hours are clock hours where 6 is sunrise and 18 is sunset, so 20 to 5 is night wrapping past midnight. maximumRate of 4 per 30 seconds becomes one attempt every 7.5 seconds for this spawner across the whole world.
4. Attach it. In biomes/tutorial/meadow.json, add the spawner key. This is a field excerpt, not a new file:
{
"entitySpawners": ["tutorial/night-zombies"]
}
5. Verify. Validate the pack first — the validator resolves the spawner-to-entity edge and will name the broken link before you load a world. Then open Studio, focus tutorial/meadow, set night, and stand somewhere with block light under 8. Success is named zombies appearing within a few seconds and dropping iron nuggets when killed. On Bukkit you can prove the entity file loads on its own with /iris studio spawn tutorial/zombie; on Fabric/Forge/NeoForge that command is not registered and reports that it is Bukkit-only, so go straight to ambient spawning.
6. If nothing spawns. Work down the gate list in order rather than raising rarity or the rate:
- World-wide crowding. If living entities divided by loaded chunks exceeds
world.targetSpawnEntitiesPerChunk(0.95 by default, scaled by 1.28), Iris stops spawning for five seconds and logs it under debug. A test world full of mobs will starve your spawner. - Chunk crowding.
maxEntitiesPerChunkis compared against the living entities already in that chunk. - Time and weather. Both are read from the world at attempt time.
- Light. The check only runs when
allowedLightLevelsis narrower than 0-15, and it reads the combined maximum of sky and block light, not block light alone. Amax: 7spawner will not fire on a surface block in daylight. - Group versus biome. A
NORMALspawner listed on a dimension is rejected in sea, shore, and cave biomes. Region- and biome-level spawners skip that check entirely and fire wherever their parent applies, so a mismatchedgroupthere produces mobs at odd heights rather than no mobs. - Placement viability. The chosen block's
surfacemust match the entity'ssurface, and the entity's bounding box must be clear air.
Where files live
| Path | Class | Role |
|---|---|---|
loot/<key>.json |
IrisLootTable |
A pool of items with per-entry rarity, rolled as a unit |
entities/<key>.json |
IrisEntity |
One mob: type, gear, flags, drops, passengers |
spawners/<key>.json |
IrisSpawner |
The gates and rates that decide when entity keys appear |
markers/<key>.json |
IrisMarker |
A tag that attaches spawners to individual blocks |
Keys are the pack-relative path without .json, so loot/tutorial/dungeon-cache.json is referenced as tutorial/dungeon-cache.
What references what
| Holder | Field | Effect |
|---|---|---|
| Dimension / region / biome | loot (IrisLootReference) |
Contributes tables to every container in that scope |
| Object placement | loot (IrisObjectLoot[]) |
Contributes one table to containers the object placed |
| Object placement | vanillaLoot (IrisObjectVanillaLoot[]) |
Same, but the table is a real Minecraft loot table |
| Object placement | overrideGlobalLoot |
Discards the dim/region/biome contribution for that placement |
| Entity | loot |
Replaces the mob's vanilla drop table |
| Dimension / region / biome | entitySpawners |
Runs those spawners for chunks in that scope |
| Object placement | markers (IrisObjectMarker[]) |
Tags matching blocks the object placed |
| Marker | spawners |
Runs those spawners at every block carrying the tag |
| Dimension / region / biome | blockDrops (IrisBlockDrops[]) |
Adds or replaces drops when a player breaks a matching block |
Ambient spawning needs world.ambientEntitySpawningSystem true; marker spawning needs world.markerEntitySpawningSystem true. Both default to true. Iris spawners run alongside vanilla and mod spawning rather than replacing it, and nothing deduplicates between the two — a zombie spawner in your pack adds to whatever the server would have spawned anyway.
The shipping Overworld pack wires its spawners at region scope (regions/*.json list <climate>/cave, /hostile, /passive, /water) and ships no markers/ folder. Its dimension-level loot uses FALLBACK, so global-clutter only reaches chests that nothing else claimed.
How a container actually gets filled
Worth reading before tuning rarities, because two of these steps surprise people.
-
Trigger. After a chunk's mantle materializes tiles and custom blocks, the post-load update pass visits every block Iris flagged. Only storage chests proceed, and only the
STORAGEslot type is ever requested. Objects placed into an already-live world (Studio placement, WorldEdit-driven placement) fill their chests immediately instead, on the region thread that owns the chunk. -
Source list. Iris asks the object placement that owns the block for at most one table. Candidates are bucketed by how specifically they match: entries with an
exactblock-data filter that matches win outright; failing that, entries whose filter matches the block's material; failing that, entries with no filter. Within the winning bucket the pick is weighted byweight.lootandvanillaLootentries compete in the same buckets. If the placement setsoverrideGlobalLootand a table was picked, that's the entire list. -
Environment sources. Otherwise the dimension, region, and surface biome each inject their tables in that order, and a cave biome injects too when the container is below terrain height and resolves to a different biome than the surface.
ADDappends.CLEARandREPLACEboth wipe the list first and then append their own tables — despite the field description,CLEARdoes not suppress its own tables.FALLBACKinjects only when nothing already claimed the container, meaning neither the placement'sloot/vanillaLootnor a pre-existing vanilla loot table on the block. -
Multiplier. The multipliers from every contributing scope are multiplied together, and the resulting factor scales the length of the table list, not stack sizes. A factor of 0.5 randomly drops half the tables; a factor of 2 randomly duplicates entries until the list doubles. The list is capped at 256 sources and Iris throws rather than silently truncating past that.
-
Event hook. On Bukkit,
art.arcane.iris.core.events.IrisLootEventfires with the engine, block, slot type, and the resolved table list. The list is mutable, so a plugin can add, remove, or clear tables before the roll. Iris also bridges the rolled items through Bukkit'sLootGenerateEvent, so plugins that already listen for vanilla loot generation see Iris chests too. This is not part of the documented public API surface in the90-series pages. -
Roll. For each table: pick a random target count between
minPickedandmaxPicked, then loop up tomaxTries. Each try picks a random entry index. The entry only counts if itsslotTypesisSTORAGEand it passes a 1-in-(tablerarityx entryrarity) check. That check is derived from the loot seed, the table's key, the entry index, and the block coordinates — not from a running random sequence. The same entry at the same block always gives the same answer, so tries that re-roll a rare entry that already failed are wasted. This is whymaxTriesshould sit well abovemaxPickedwhen a table has rare entries. -
Scatter. Items are inserted, then one multi-item stack is split into a free slot and all slots are shuffled, so a chest reads like a hand-placed one instead of a left-packed block of stacks.
Modded servers run the same resolver and the same rarity math, so container contents match Bukkit for a given seed and position. Modded resolves vanillaLoot names against the server's loot-table registry directly; Bukkit resolves them through Bukkit.getLootTable and delegates the roll to Minecraft.
Loot tables (IrisLootTable)
Folder: loot/.
| Field | Type | Default | What it does |
|---|---|---|---|
name |
string | "" |
Human label shown in Studio debug lore. Required, at least 2 characters. Not the lookup key — that's the file path |
rarity |
int >= 1 | 1 |
Multiplied into every entry's rarity. Raise it to make a whole table rare without editing each entry; leave at 1 and tune entries individually |
minPicked |
int 0..64 | 1 |
Floor of the random target count. Set to 0 when a chest is allowed to come out empty |
maxPicked |
int 1..64 | 5 |
Ceiling of the random target count. This is the most items one table can contribute |
maxTries |
int 1..256 | 10 |
How many entry draws are allowed before the table gives up. Raise it when entries have high rarity, or the table will routinely undershoot minPicked |
loot |
IrisLoot[] |
[] |
The entries. An empty table contributes nothing and is not an error |
Loot entry (IrisLoot, snippet type loot)
| Field | Type | Default | What it does |
|---|---|---|---|
type |
string | "" |
Required. A plain material name (diamond, DIAMOND_SWORD). Namespaced values are reserved for items from other plugins or mods, resolved through the external-data service — minecraft: prefixes are not the pattern here |
slotTypes |
InventorySlotType |
STORAGE |
Which inventory slot family the entry targets. Generated containers only ever request STORAGE, so FUEL, FURNACE, BLAST_FURNACE, and SMOKER entries are inert in world generation |
rarity |
int >= 1 | 1 |
1-in-N chance for this entry, multiplied by the table's rarity. Use it to make one entry rare inside an otherwise common table |
minAmount / maxAmount |
int 1..64 | 1 / 1 |
Inclusive stack-size range rolled per pick |
displayName |
string | null | Item name. & colour codes are translated |
lore |
string[] | [] |
Lore lines. Lines over 24 characters are word-wrapped into several lines |
minDurability / maxDurability |
0..1 | 0 / 1 |
Fraction of durability remaining, rolled per pick. 0/1 gives anything from nearly broken to pristine; set both to 1 for undamaged gear |
customModel |
int | null | Custom model data, written into the item's model-data component as a float. For resource packs that key off model data |
unbreakable |
boolean | false |
Marks the item unbreakable |
itemFlags |
string[] | [] |
Bukkit ItemFlag names, for hiding enchantments or attributes in the tooltip. Unrecognised names are skipped silently |
enchantments |
IrisEnchantment[] |
[] |
Each has its own level range and application chance |
attributes |
IrisAttributeModifier[] |
[] |
Attribute modifiers baked into the item |
dyeColor |
string | null | A DyeColor name, applied to items that are colourable |
leatherColor |
string | null | #RRGGBB, applied to leather armour |
customNbt |
object | null | Raw platform NBT merged into the item. Also carries the payload for namespaced third-party items |
When Studio debug is on (/iris studio loot, or any roll in a Studio world), each item gains lore naming the source table and the combined 1-in-N chance. That lore is not written in production worlds.
Loot reference (IrisLootReference, snippet type loot-registry)
The shape used by dimension, region, biome, and entity loot fields:
{
"loot": {
"mode": "FALLBACK",
"multiplier": 0.5,
"tables": ["temperate/clutter", "temperate/food"]
}
}
| Field | Type | Default | What it does |
|---|---|---|---|
mode |
IrisLootMode |
ADD |
How this scope's tables combine with the scopes above it — see below |
tables |
string[] | [] |
Loot table keys. A key that doesn't resolve is skipped |
multiplier |
double 0..16 | 1 |
Scales how many tables end up in the final list for containers in this scope. Below 1 randomly drops tables, above 1 randomly duplicates them. Multiplied across every contributing scope |
| Mode | Behaviour |
|---|---|
ADD |
Append these tables to whatever the outer scopes contributed. The default and the right choice most of the time |
REPLACE |
Wipe the list, then append these. Use on a biome that should ignore the dimension's global tables |
CLEAR |
Identical to REPLACE in code: wipe, then append these. The field description claims it also suppresses its own tables; it does not |
FALLBACK |
Only contribute when nothing already claimed the container — no object loot, no object vanillaLoot, no native loot table on the block. This is how a pack ships a broad filler table without stepping on structure chests |
Entities read only tables from this object. Setting mode or multiplier on an entity's loot has no effect.
Object loot
On IrisObjectPlacement (see 20 - Object Placement.md):
| Field | What it does |
|---|---|
loot |
IrisObjectLoot[] — Iris tables offered to containers this placement created |
vanillaLoot |
IrisObjectVanillaLoot[] — Minecraft loot table keys offered the same way |
overrideGlobalLoot |
When a table was picked from this placement, stop there and skip dimension, region, and biome tables |
Both entry types share the same fields:
| Field | Default | What it does |
|---|---|---|
name |
required | The Iris loot table key, or for vanillaLoot a namespaced Minecraft loot table key such as minecraft:chests/simple_dungeon |
weight |
1 |
Relative odds inside its match bucket. Entries with weight 0 or below are ignored entirely |
filter |
[] |
Blocks this entry applies to. Empty means every container the object placed |
exact |
false |
Compare the whole block state instead of just the material. An exact match outranks a material match, which outranks an unfiltered entry |
One table is picked per container, not one per entry — the buckets decide which entries are eligible and weight decides between them.
Authored container contents and deferred custom-block identifiers stay in the mantle until the platform's post-load materialization pass runs for that chunk. Generic cleanup and pregeneration cleanup preserve those sparse payloads for chunks that have not reached the pass yet, and on Bukkit a region-scheduled pass that fails stays retryable without repeating the passes that already completed.
Entities (IrisEntity)
Folder: entities/. A minimal entity is one field:
{ "type": "ZOMBIE" }
entities/standard/hostile/zombie.json is referenced as standard/hostile/zombie.
| Field | Type | Default | What it does |
|---|---|---|---|
type |
string | null | Required. The entity type key (minecraft:zombie, alexsmobs:grizzly_bear, or a bare name). Set it to unknown and fill specialType when the mob comes from a plugin instead |
specialType |
string | "" |
PluginName:MobName, spawned through the external-data service. Mythic Mobs and similar providers plug in here |
applySettingsToCustomMobAnyways |
boolean | false |
By default Iris hands a specialType mob straight back to its provider untouched. Turn this on to layer Iris gear, names, and flags on top |
reason |
string | null | The SpawnReason reported to other plugins. Unset or unrecognised becomes NATURAL. Change it when another plugin gates on spawn reason |
customName |
string | "" |
Name tag, with & colour codes |
customNameVisible |
boolean | false |
Show the name without looking at the mob |
aware |
boolean | true |
Whether the mob reacts to the world. Off makes a decorative mob that stands still but still animates |
ai |
boolean | true |
Whether the mob has AI goals at all. Off is a harder freeze than aware: false |
glowing |
boolean | false |
Outline visible through blocks |
gravity |
boolean | true |
Off leaves the mob floating where it spawned |
invulnerable |
boolean | false |
Only creative-mode players can damage it |
silent |
boolean | false |
Suppresses the mob's sounds |
pickupItems |
boolean | false |
Whether it can pick up dropped gear |
removable |
boolean | false |
Whether the server may despawn it when players leave. Off keeps a set-piece mob alive |
keepEntity |
boolean | false |
Forces persistence. Also forced globally by world.forcePersistEntities |
baby |
boolean | false |
Spawns the baby variant for ageable types |
helmet / chestplate / leggings / boots / mainHand / offHand |
IrisLoot |
null | One equipment slot each, built like a loot entry. The entry's own rarity is a 1-in-N roll for whether the slot gets filled at all — that's how you get "one in five wears a helmet" |
passengers |
IrisEntity[] |
[] |
Riders, spawned and mounted after the host. Nests, so a rider can carry a rider |
attributes |
IrisAttributeModifier[] |
[] |
Attribute modifiers applied to the mob |
loot |
IrisLootReference |
empty | Drop tables. Replaces the mob's vanilla drops. Only tables is read |
leashHolder |
IrisEntity |
null | Spawns a second entity and leashes this one to it. No effect on ender dragons, withers, players, or bats |
spawnEffect |
IrisEffect |
null | A one-shot effect fired at the spawn position |
spawnEffectRiseOutOfGround |
boolean | false |
Spawns the mob five blocks lower when a player is nearby and walks it up out of the ground with block-crack particles. The mob is invulnerable and AI-less for up to five seconds while rising |
pandaMainGene / pandaHiddenGene |
string | null | Panda genes. Unrecognised names fall back to NORMAL |
surface |
IrisSurface |
LAND |
What the block under the spawn point must be — see below. Marker-driven spawns skip this check |
rawCommands |
IrisCommand[] |
[] |
Console commands run after the mob spawns |
IrisSurface values, checked against the block directly below the spawn position:
| Value | Matches |
|---|---|
LAND |
Any solid block |
ANIMAL |
Grass block, dirt, dirt path, coarse dirt, rooted dirt, podzol, mycelium, or snow block. Narrower than LAND on purpose, for passive mobs |
WATER |
Water, seagrass, kelp, or any waterlogged block |
OVERWORLD |
Any solid block or any water block — use it when a spawner should work on shore and in shallows |
LAVA |
Lava |
Entity drops in practice
Both platforms replace the mob's vanilla drop table rather than adding to it, and the tables are rolled at the mob's spawn coordinates with the STORAGE slot type. Because entry rarity is position-derived, every mob spawned on the same block rolls identical drops. Vary minAmount/maxAmount if you want visible variation from a single-entry table.
On Bukkit a synthetic loot table is bound to the mob. On modded the mob carries an iris_loot|… tag and Iris emits the items on death instead of the base table. Modded also fills chest-carrying vehicles directly at spawn; for any other entity type that exposes no lootable path it logs one warning per type and skips.
Entity commands (IrisCommand, snippet type command)
rawCommands runs console commands after a mob spawns. The same object is used by ambient effect command registries.
| Field | Default | What it does |
|---|---|---|
commands |
[] |
Required. Command strings. A leading / is stripped, and {x}, {y}, {z} are replaced with the spawn block coordinates |
delay |
0 |
Server ticks before the first run. Negative values clamp to zero |
repeat |
false |
Repeat forever after the first run. There is no cancel handle, and repeats do not survive a restart — they only exist for as long as the server stays up after the chunk generated |
repeatDelay |
100 |
Server ticks between repeats. Values below 1 clamp to 1 |
timeBlock |
any time | World-time window the command is allowed in |
weather |
ANY |
Required weather: NONE, DOWNFALL, DOWNFALL_WITH_THUNDER, or ANY |
timeBlock and weather are evaluated once, when the command object first runs. A repeating command keeps repeating after its window closes.
Bukkit and modded both apply AI and awareness flags, spawn effects, and raw commands, and both enforce spawner time and weather gates.
Ambient effects (IrisEffect, snippet type effect)
Biomes and regions accept effects[]. Each entry runs at most once per interval milliseconds and, when it runs, has a 1-in-chance shot at firing. A single entry can apply a potion, play a sound, emit particles, and run commands. The whole system is gated by world.effectSystem in settings.json.
| Field | Default / range | What it does |
|---|---|---|
interval |
150 ms, >= 0 |
Minimum gap between attempts. Raise it for anything expensive or loud |
chance |
50, >= 1 |
One attempt in this many actually fires. Combined with interval this is your real frequency |
potionEffect |
"" |
Potion effect registry key. An unknown key falls back to LUCK and logs a warning |
potionStrength |
-1 (-1..1024) |
Amplifier. -1 disables potion application entirely, which is the default |
potionTicksMin / potionTicksMax |
75 / 155 |
Random potion duration in ticks |
sound |
null | Sound registry key |
soundDistance |
12 (0..512) |
How far from the player the sound origin can be offset. Larger values make the source feel distant and directionless |
minPitch / maxPitch |
0.5 / 1.5 (0.01..1.99) |
Random pitch range |
volume |
1.5 (0.001..512) |
Sound volume |
particleEffect |
null | Particle registry key. Modded supports particle types that need no extra particle data |
particleOffset |
0 (-32..32) |
Random vertical offset from the sampled surface |
particleCount |
0 (0..512) |
Particle count. Zero is meaningful: on Bukkit it makes the alt XYZ values behave as velocity instead of spread |
particleDistance |
20 (0..64) |
How far ahead of the player particles are sampled |
particleDistanceWidth |
24 (0..128) |
Sampling radius left and right of the player |
particleAway |
5 (0..16) |
Minimum forward offset, so particles don't spawn in the player's face |
particleAltX / particleAltY / particleAltZ |
0 (-8..8) |
Spread, or velocity when particleCount is 0 |
randomAltX / randomAltY / randomAltZ |
true / false / true |
Randomize each alt component between its negative and positive value. Y defaults off so vertical drift stays deliberate |
extra |
0 |
Particle-specific extra value, meaningful only for some particle types |
commandRegistry |
null | Commands to cast alongside the effect |
IrisCommandRegistry (snippet type command-registry):
| Field | Default | What it does |
|---|---|---|
rawCommands |
[] |
IrisCommand[] to run |
commandOffsetX / commandOffsetY / commandOffsetZ |
0 (-8..8) |
Offsets from the player for the {x} {y} {z} substitutions |
commandRandomAltX / commandRandomAltY / commandRandomAltZ |
true / false / true |
Randomize each coordinate within its signed offset |
commandAllRandomLocations |
true |
Re-roll the coordinate for each command object. Set false to fire every command at one shared point |
{
"particleEffect": "minecraft:ash",
"particleCount": 8,
"sound": "minecraft:ambient.cave",
"interval": 1000,
"chance": 8,
"commandRegistry": {
"commandOffsetX": 4,
"commandOffsetZ": 4,
"rawCommands": [{ "commands": ["particle minecraft:smoke {x} {y} {z}"] }]
}
}
Spawners (IrisSpawner)
Folder: spawners/.
| Field | Type | Default | What it does |
|---|---|---|---|
spawns |
IrisEntitySpawn[] |
[] |
The ongoing spawn pool. Every entry competes with entries from every other eligible spawner in the same chunk |
initialSpawns |
IrisEntitySpawn[] |
[] |
A separate pool used once per chunk, the first time that chunk is maintained. For set dressing that should exist from the moment a chunk appears |
maxEntitiesPerChunk |
int | 1 |
Skip this spawner when the target chunk already holds this many living entities. The single most common reason a spawner looks dead — the default of 1 means almost any occupied chunk blocks it |
timeBlock |
IrisTimeBlock |
any time | World-time window. Clock hours, 6 = sunrise, 18 = sunset |
weather |
IrisWeather |
ANY |
NONE, DOWNFALL, DOWNFALL_WITH_THUNDER, or ANY |
maximumRate |
IrisRate |
infinite | World-wide throttle for this spawner. Stamped only when a spawn actually succeeds |
maximumRatePerChunk |
IrisRate |
infinite | Same throttle, tracked per chunk. Use it to stop one chunk hogging a generous global rate |
allowedLightLevels |
IrisRange |
0..15 |
Inclusive light range. Skipped entirely when left at the full range. Measured as the combined maximum of sky and block light |
group |
IrisSpawnGroup |
NORMAL |
Where in the column mobs are placed, and which biomes accept this spawner at dimension scope |
IrisSpawnGroup:
| Value | Position chosen | Biome check (dimension scope only) |
|---|---|---|
NORMAL |
Random x/z in the chunk, one block above the fluid-inclusive surface | Land biomes only |
CAVE |
A random cave_floor mantle marker in the chunk, one block up |
Accepted in every biome type |
UNDERWATER |
Random x/z, random Y between the solid top and the water surface | Sea biomes only |
BEACH |
Same water-column position as UNDERWATER |
Shore biomes only |
The biome check only applies to spawners listed on a dimension. Region and biome entitySpawners bypass it, so a CAVE-group spawner listed on a surface biome will still try to find cave floor markers there and quietly do nothing if there are none.
On Folia, CAVE group spawners never fire on Bukkit — the cave-floor marker lookup returns nothing off the region thread and the spawn is skipped.
IrisRate (snippet type rate):
| Field | Default | What it does |
|---|---|---|
amount |
0 |
How many firings the duration allows. The effective cooldown is per divided by amount (or by 1 when amount is 0) |
per |
empty | The window. An empty per means unlimited — that's what makes a rate infinite, not amount |
IrisDuration (snippet type duration) sums every field you fill in:
| Field | Real time per unit |
|---|---|
milliseconds |
1 ms |
minecraftTicks |
50 ms |
seconds / minutes / hours / days |
Real-world units |
minecraftHours |
50 s |
minecraftDays |
20 min |
minecraftWeeks |
2 h 20 min (7 Minecraft days) |
minecraftLunarCycles |
2 h 40 min (8 Minecraft days) |
IrisTimeBlock (snippet type time-block): startHour and endHour in 24-hour clock time, where the world's tick 0 reads as hour 6. Setting both to the same value means any time; setting both to -1 means never. A startHour greater than endHour wraps past midnight.
Entity spawn entry (IrisEntitySpawn, snippet type entity-spawn)
| Field | Type | Default | What it does |
|---|---|---|---|
entity |
string | "" |
Required. The entity key |
rarity |
int >= 1 | 1 |
Inverse weight. All eligible entries from all eligible spawners go into one pool and each entry gets totalRarity / rarity slots, so low numbers are common and high numbers are rare. Exactly one entry wins per chunk attempt |
minSpawns / maxSpawns |
int >= 1 | 1 / 1 |
Inclusive range of placement attempts once this entry wins. Each attempt can still fail the surface, light, or clearance check, so this is a ceiling not a guarantee |
Modded applies rarity twice — once as the pool weight and again as a 1-in-N roll at each candidate position — so high-rarity entries spawn somewhat less often on Fabric/Forge/NeoForge than on Bukkit for the same numbers.
Real Overworld spawner, spawners/temperate/hostile.json:
{
"group": "NORMAL",
"maximumRate": { "amount": 8, "per": { "seconds": 15 } },
"timeBlock": { "startHour": 20, "endHour": 3 },
"maxEntitiesPerChunk": 2,
"weather": "ANY",
"spawns": [
{ "entity": "standard/hostile/zombie", "rarity": 5, "maxSpawns": 4, "minSpawns": 2 },
{ "entity": "standard/hostile/skeleton", "rarity": 30, "maxSpawns": 2, "minSpawns": 1 },
{ "entity": "standard/neutral/enderman", "rarity": 60, "maxSpawns": 2, "minSpawns": 1 }
]
}
Attach it on a dimension, region, or biome:
{
"entitySpawners": ["temperate/hostile", "temperate/passive"]
}
The ambient tick
The loop runs once per Iris world every world.asyncTickIntervalMS milliseconds (700 by default, 3000 when both spawn systems are off). Each pass:
- Recount living entities in the world, throttled so it doesn't run every tick. If the count can't be completed — the scheduler refuses the task, or it times out — Iris pauses spawning entirely rather than guessing. This is deliberate: an incomplete count must never authorize a spawn.
- Compute saturation as living entities divided by loaded chunks plus one, scaled by 1.28. Above
world.targetSpawnEntitiesPerChunkthe pass sleeps 5 seconds and returns. - Pick between 2 and 12 random loaded chunks and run one spawn attempt in each, on the region thread that owns the chunk.
- Pregeneration and world maintenance suppress spawning for that world entirely while they run.
In Studio worlds, spawning additionally requires studio.entitySpawning.
initialSpawns runs from the chunk-maintenance pass, once per chunk, guarded by a mantle flag so it never repeats. That pass returns early when world.markerEntitySpawningSystem is off, so initialSpawns needs both spawn settings enabled even though it isn't marker-driven.
Markers (IrisMarker)
Folder: markers/. A marker is a tag written into the mantle at a block position; the marker file says which spawners fire there.
| Field | Type | Default | What it does |
|---|---|---|---|
spawners |
string[] | [] |
Spawner keys. One is picked at random each time the marker fires |
removeOnChange |
boolean | true |
Delete the marker when a player breaks the block it sits on. Leave on unless you want a spawn point that survives being mined out |
emptyAbove |
boolean | true |
Require two non-solid blocks above. Checked twice — see below |
exhaustionChance |
double | 0 |
Odds the marker deletes itself when it fires. 0.25 averages four uses. Anything at or below 0 never exhausts; 1 or higher exhausts on the first use |
emptyAbove is checked at two different times against two different things. When the object is placed, Iris asks whether the object itself defines blocks one and two above the candidate — so a marker on a floor block under the object's own ceiling is never written. When the marker later fires, the scanner re-checks against the live world, and a marker that has since been buried is deleted from the mantle rather than skipped.
Placing markers from objects (IrisObjectMarker, snippet type object-marker)
On an object placement's markers[]:
| Field | Type | Default | What it does |
|---|---|---|---|
mark |
IrisBlockData[] |
required | Block types to tag. Candidate blocks are shuffled, so which matching blocks get tagged varies by placement |
marker |
string | required | The marker key to attach |
maximumMarkers |
int 1..16 | 8 |
Cap for this entry across all its mark types. Keep it low — each marker is a per-block mantle write and a per-chunk scan cost |
exact |
boolean | false |
Match the full block state instead of just the material |
{
"place": ["dungeons/crypt"],
"chance": 0.01,
"markers": [
{ "mark": [{ "block": "minecraft:spawner" }], "marker": "crypt-spawns", "maximumMarkers": 4 }
]
}
When marker spawning is on, each chunk pass reads the mantle markers in that chunk, skips the engine's internal cave_floor and cave_ceiling tags, loads each IrisMarker, drops obstructed ones, picks one of that marker's spawners at random, and fires it at the marker position. Marker spawns bypass the entity's surface check and the bounding-box clearance check — the marker is taken as authoritative about the position being valid — but they still honour allowedLightLevels and the spawner's time, weather, and rate gates. exhaustionChance is rolled once per firing, before the mobs are placed.
Custom block drops (IrisBlockDrops, snippet type block-drops)
Dimensions, regions, and biomes accept blockDrops[]. When a player breaks a block, matching providers from the biome run first; unless a matching biome provider sets skipParents, matching region and then dimension providers are appended.
| Field | Default | What it does |
|---|---|---|
blocks |
[] |
Required. Block types this rule reacts to |
exactBlocks |
false |
False matches on material alone, so any barrel matches minecraft:barrel. True requires the full block state, so minecraft:barrel[axis=x] matches only that orientation |
drops |
[] |
IrisLoot[]. Each entry rolls its own rarity independently — unlike loot tables there's no pick count or try budget, so every entry gets exactly one chance |
skipParents |
false |
On a matching biome provider, stops region and dimension providers running for this break. Use it when a biome needs to fully own a block's drops |
replaceVanillaDrops |
false |
If any matching provider sets this, vanilla drops are suppressed while Iris drops from every selected provider still fire |
{
"blocks": [{ "block": "minecraft:stone" }],
"exactBlocks": false,
"drops": [{ "type": "flint", "rarity": 4 }],
"skipParents": false,
"replaceVanillaDrops": false
}
This runs on Bukkit and on Fabric, Forge, and NeoForge. Each loader hooks its own block-break path and routes the result through the same rule evaluation.
Runtime settings that gate these systems
From settings.json under world (see 03 - Configuration.md):
| Key | Default | Effect |
|---|---|---|
postLoadBlockUpdates |
true |
Drives the pass that fills generated containers. Off means no chest loot |
ambientEntitySpawningSystem |
true |
Dimension, region, and biome entitySpawners |
markerEntitySpawningSystem |
true |
Marker-driven spawners, and the chunk pass that runs initialSpawns |
effectSystem |
true |
Biome and region effects[] |
targetSpawnEntitiesPerChunk |
0.95 |
Saturation ceiling. Lower it on busy servers to stop Iris adding to entity load |
asyncTickIntervalMS |
700 |
How often the spawn loop runs per world |
forcePersistEntities |
true |
Marks every Iris-spawned entity persistent regardless of keepEntity |
Checklist
- Write
loot/<key>.jsontables. GivemaxTriesroom abovemaxPickedif any entry is rare. - Reference them from
loot.tableson a dimension, region, or biome, or fromloot[].nameon an object placement. - Write
entities/<key>.jsonwith at leasttype. Setsurfaceto match where the mob belongs. - Write
spawners/<key>.json. RaisemaxEntitiesPerChunkabove the default of 1 unless you genuinely want one mob per chunk. - Add spawner keys to
entitySpawnerson a dimension, region, or biome, or attach them through markers on an object placement. - Validate the pack — the spawner-to-entity edge is a blocking check, so a typo is caught before you load a world.
- Open Studio and confirm in-world. Use
/iris studio lootfor chest previews on Bukkit.