-
-
Notifications
You must be signed in to change notification settings - Fork 0
How It Works
This page explains what a managed spawner actually does, so the numbers in your config make sense before you tune them.
A vanilla spawner produces entities. Entities tick, pathfind, collide, and are the single biggest source of lag on a spawner-farm server.
A managed spawner skips the entity entirely. Each cycle it asks: "if these mobs had spawned and died, what would have dropped?" — rolls the drop table, and writes the answer into a database row. No mob is ever created.
The cost of a 100,000-stack spawner is therefore the same as a 1-stack spawner: one row update every few seconds.
A repeating task runs every SETTINGS.GENERATION_INTERVAL_SECONDS (default 5). Its period is
clamped to a minimum of 1 second (20 ticks). For every managed spawner it schedules work on the
region that owns that block — which is what makes the plugin Folia-native — and then runs these
checks in order:
1. Is the spawner type still defined in spawners.yml? no -> skip
2. Is the world loaded? no -> skip
3. PROCESS_ONLY_LOADED_CHUNKS and the chunk is unloaded? yes -> skip
4. REQUIRE_PLAYER_NEARBY and nobody within radius? yes -> skip
5. Is the block still actually a SPAWNER? no -> skip
6. HOPPER_EXTRACTION enabled? yes -> push loot into the hopper below
7. Has a full interval elapsed since last_processed_at? no -> stop here
8. Generate.
Steps 3 and 4 are your two big performance levers — see Performance Tuning.
The spawner does not lose time between ticks. It stores last_processed_at and computes:
cycles = (now - last_processed_at) / interval_millis
A spawner that was skipped never advances its clock, because steps 3 and 4 return early before
last_processed_at is touched. The elapsed time keeps accumulating and is paid out in one lump
the moment the spawner becomes eligible again.
Consequence worth knowing: with
PROCESS_ONLY_LOADED_CHUNKS: true, a farm that sat unloaded for a week will dump a week of loot into storage the moment its chunk loads — up to the per-material cap. That is usually what players want, but it is a spike, not a trickle.
After a successful generation the clock advances by exactly cycles x interval, not to now, so
no fractional time is ever lost or double-counted.
Per cycle, the total number of drop-table rolls is:
totalRolls = cycles x BASE_ITEMS_PER_CYCLE x stackAmount
Then, for each drop entry that is not filtered off:
expected = totalRolls x CHANCE x ((MIN + MAX) / 2)
generated = floor(expected)
+ 1 with probability (expected - floor(expected))
In other words the plugin computes the statistical expectation and rolls only for the leftover fraction, instead of rolling once per mob. This is what makes a 100,000-stack spawner as cheap as a 1-stack one — the cost does not scale with the stack size.
A 200-stack ZOMBIE spawner, default config, 60 seconds of elapsed time:
interval = 5 s -> cycles = 12
baseItems = 1
stack = 200
totalRolls = 12 x 1 x 200 = 2400
ROTTEN_FLESH CHANCE 1.0 MIN 1 MAX 3 -> avg 2.0 -> 2400 x 1.0 x 2.0 = 4800
IRON_INGOT CHANCE 0.05 MIN 0 MAX 1 -> avg 0.5 -> 2400 x 0.05 x 0.5 = 60
CARROT CHANCE 0.025 MIN 0 MAX 1 -> avg 0.5 -> 2400 x 0.025 x 0.5 = 30
POTATO CHANCE 0.025 MIN 0 MAX 1 -> avg 0.5 -> 2400 x 0.025 x 0.5 = 30
So roughly 4,800 rotten flesh, 60 iron, 30 carrots and 30 potatoes per minute.
Use this formula to sanity-check a new type before you ship it — it is very easy to configure a spawner that prints money.
XP is generated on the same cycle, and is a flat rate rather than a roll:
xp = cycles x XP_PER_CYCLE x stackAmount
XP_PER_CYCLE defaults to 3.7 and can be overridden per type. XP is stored as a decimal and
rounded to the nearest whole point only when a player claims it. Set SETTINGS.XP_ENABLED: false
to switch generation and collection off entirely.
Each spawner owns a set of loot entries — one per material, each with an amount, capped at
STORAGE_CAP_PER_LOOT_KEY (default 1,000,000). The cap is per material, not per spawner: a
zombie spawner can hold a million rotten flesh and a million iron ingots.
Once a material hits the cap, further generation of that material is discarded. Nothing errors and nothing is logged — so if players report that a spawner "stopped working", check the cap first.
Storage is presented as a paged inventory GUI. See Storage and Filters.
Every drop can be toggled off per spawner. A disabled material is:
- not generated — it is skipped during the cycle, so it never enters storage,
- not sold by Sell All,
- not dropped by Drop Loot,
- not extracted by a hopper.
Disabled keys are stored on the spawner row as a text list, so they survive restarts.
Two things mark a managed spawner, and they are deliberately redundant:
-
A database row keyed on
(world, x, y, z)— the source of truth, holding stack size, owner, access mode, stored XP and disabled filters, with the loot in a second table. -
Persistent data on the block itself — a marker byte plus type, stack amount, owner UUID,
owner name and access mode, written into the spawner block's
PersistentDataContainer.
If a spawner is missing from the database but the block still carries its marker — the usual cause is a world rollback or a restore from backup — the plugin adopts it back. It recreates the database row from the block data and logs:
[SpawnerManager] Restored a stray ZOMBIE spawner at world:100,64,-200 from its block data;
its stored loot could not be recovered.
Stack size, type and owner come back. The stored loot does not — it only ever lived in the database.
With SETTINGS.CANCEL_MOB_SPAWN: true (the default), three layers stop physical mobs:
-
SpawnerSpawnEventis cancelled for any managed spawner, and the entity is removed. -
CreatureSpawnEventwith reasonSPAWNERis cancelled within 12 blocks of any managed spawner, as a backstop for anything that slipped past the first check. - The spawner block's own vanilla logic is disabled in its block state
(
maxNearbyEntities = 0).
Layer 3 is the important one: it is written into the world, not held in memory. A managed spawner stays inert across restarts — and even if the plugin is removed.
Managed spawners are also filtered out of explosion block lists, so creepers and TNT cannot destroy a stack.
+--------------------------+
every 5 s -------> | SpawnerGenerationTask |
+------------+-------------+
| per spawner, on its own region thread
v
+--------------------------+
| roll drop table + XP |
+------------+-------------+
v
right-click --> GUI <-- virtual storage (in memory)
| async write
v
+--------------------------+
| SQLite / MySQL |
| uvs_spawners |
| uvs_spawner_loot |
| uvs_balances |
+--------------------------+
Database writes are pushed onto a dedicated daemon thread, so a slow disk or a distant MySQL server never stalls a game tick.
- spawners.yml — every setting named above
- Performance Tuning — what to change on a large server
- Database — the schema in full
Repository · Releases · Issues · Discord · MIT License
Getting started
Reference
Configuration
Features
Operations
Development