Skip to content

How It Works

BeestoXd edited this page Aug 15, 2026 · 1 revision

How It Works

This page explains what a managed spawner actually does, so the numbers in your config make sense before you tune them.

The core idea

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.

The generation cycle

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.

Catch-up

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.

The drop maths

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.

Worked example

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

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.

Storage

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.

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.

How a spawner is identified

Two things mark a managed spawner, and they are deliberately redundant:

  1. 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.
  2. 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.

Blocking real mobs

With SETTINGS.CANCEL_MOB_SPAWN: true (the default), three layers stop physical mobs:

  1. SpawnerSpawnEvent is cancelled for any managed spawner, and the entity is removed.
  2. CreatureSpawnEvent with reason SPAWNER is cancelled within 12 blocks of any managed spawner, as a backstop for anything that slipped past the first check.
  3. 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.

Data flow at a glance

                        +--------------------------+
    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.

Related pages

Clone this wiki locally