No. 06 · Business · 2026
Generators SMP
A DonutSMP-style economy Minecraft server, run on one custom plugin: generators that earn on a timer, raids, an auction house and crates, in 12,600 lines of Java 21 on Paper.
- Role
- Owner · developer
- Stack
- Java 21 · Paper
- Ran
- Jun–Oct 2026
In this issue
- (1)The production model
- (2)Ticking only what's loaded
- (3)The outage at three players
- (4)Decided before the reel spins
- (5)Running it
- (6)The plan for more players
Generators SMP · Generator block
GenSMP was a DonutSMP-style Minecraft server. Players placed generators that made money on a timer, raided each other’s, traded on an auction house and opened crates for rarer generators. All of it ran on one plugin I wrote for Paper 1.21: 12,600 lines of Java 21 across 85 files, covering generators, raids, the auction house, crates, shops, bounties, leaderboards, voting and referrals.
This issue is about how that plugin works, starting with the generator itself.
The production model
A generator is two things in the world: a spawner block for the cage, and a display entity inside it showing the resource block, scaled to 0.4 and turning once every six seconds. Everything else is data. Each generator has a type, a tier from 1 to 10, a stack of up to 16, and a count of the items it’s holding.
Once a second, every generator in a loaded chunk produces its type’s rate for its tier, times its stack, times a global multiplier of 0.02. That multiplier is what makes the game slow: a Tier 1 generator makes one item every 50 seconds. Fractions build up in an accumulator on each generator, and whole items drop into storage.
Storage is capped at 480 items at Tier 1, plus 330 for each tier above it, times the stack. A full generator stops producing, so the cap sets the rhythm of the game. Most generators fill in six to ten hours, and the money a full one isn’t making is the reason to come back and sell.
Run a generator
The server's real numbers, sped up. You start where every new player did: one Tier 1 Dirt generator and $0.
- Making
- Full in
- Stored
- Next item
- Missed while full
Two rules shaped the rest. A generator only produces while its chunk is loaded, and there’s no catch-up for the time it spent unloaded, so nobody banks a week of income by leaving. And an upgrade costs the same for a stack of 16 as for one generator, so the cheapest path is to stack first and upgrade after.
Ticking only what's loaded
The first tick loop walked every generator ever placed, every second, and copied a location object for each one to check its chunk. Its cost grew with everything anyone had ever built, including generators in parts of the world nobody was near.
The fix was an index from world to chunk to the generators in that chunk, kept up to date when a generator is loaded, placed or picked up. Each tick now walks only the chunks that hold generators, skips the ones that aren’t loaded, and ticks what’s left. The cost follows where players are, not how much has been built. The same index answers the chunk-load event that respawns a chunk’s display entities, so that became a lookup instead of a scan.
Smaller fixes followed the same idea. The spin animation skips display entities no player can see. Opening a generator’s menu finds it by ID instead of searching a list. And the visuals survive restarts three ways: a full restore a second after startup, a per-chunk restore when a chunk loads, and a self-heal inside the production tick, after a startup sweep removes any display entity that no longer has a generator behind it.
The outage at three players
Everything persists to SQLite through a HikariCP connection pool: generators, earnings, auction listings, raids, referrals, votes and crate keys. The first configuration pinned that pool to a single connection with no write-ahead log. With about three players online, the server started logging Connection is not available, request timed out after 30000ms. Every system was queueing for the same connection, and each one that had to wait held its thread for the full 30 seconds.
The fix was four settings. SQLite moved to WAL mode, so reads stop blocking behind a write. The pool went from one connection to four, which WAL makes safe. A five-second busy timeout lets a writer wait for a lock instead of failing. And the pool’s connection timeout dropped from 30 seconds to 8, so a problem fails fast instead of hanging a thread.
Behind those settings is the rule the rest of the backend follows: the thread that runs the game never waits on the disk. Generators that change are marked dirty and written together in one transaction every 30 seconds, and a sale or an upgrade saves its generator straight away on the async pool. Leaderboards are read in the background and served from memory. An auction purchase claims its listing with one conditional update, UPDATE … WHERE id = ? AND status = 0, so two buyers can’t both get the item.
Decided before the reel spins
A crate opens with a reel: a row of nine slots scrolls left, fast at first and then slowing, and stops on the prize. The prize is chosen before the reel starts, by one weighted draw from the crate’s loot table. Every other item on the reel is drawn from the same table, so what flashes past is a fair sample of what the crate holds. The reel itself is only timing: 25 steps that start at one server tick each and slow to eight, about three seconds, then a one-second pause before the prize is handed over.
Each of the four crates has its own table. The Voting crate is weighted heavily toward Dirt, and Gen Ultra moves its weight toward the rarer generators. Right-clicking a crate without a key opens a preview with every prize’s exact chance, and a prize at or above each crate’s threshold is announced to the whole server.
Open a crate
Each crate's real loot table. The prize is drawn before the reel moves; the reel only takes you to it.
Opening Voting Crate…
Press Open.
Voting crate odds · none opened yet
- Dirt 44.1%
- Cobblestone 26.4%
- Coal 15.4%
- Iron 7.9%
- Gold 3.5%
- Redstone 1.5%
- Diamond 0.66%
- Emerald 0.26%
- Ancient Debris 0.09%
- Netherite 0.04%
Announced to the serverNot announcedYour opens
See the numbers
| Generator | Voting | Gen+ | Gen++ | Gen Ultra |
|---|---|---|---|---|
| Dirt | 44.1% | 28.5% | 15.0% | 9.0% |
| Cobblestone | 26.4% | 22.8% | 15.0% | 10.0% |
| Coal | 15.4% | 17.1% | 15.0% | 10.9% |
| Iron | 7.9% | 11.4% | 15.0% | 12.7% |
| Gold | 3.5% | 8.5% | 13.5% | 13.6% |
| Redstone | 1.5% | 5.7% | 11.3% | 13.6% |
| Diamond | 0.66% | 3.4% | 7.5% | 11.8% |
| Emerald | 0.26% | 1.7% | 4.5% | 9.0% |
| Ancient Debris | 0.09% | 0.68% | 2.3% | 6.3% |
| Netherite | 0.04% | 0.23% | 0.90% | 3.2% |
| Total weight | 2,269 | 1,756 | 1,332 | 1,105 |
Running it
The server ran on one Hetzner machine with two dedicated vCPUs and 8 GB of memory, as a systemd service that restarts after a crash or a reboot. Its console was reached over RCON, bound to localhost and firewalled.
Development happened against a staging copy on the same machine, on its own port, with the store and the vote listener switched off. Only a built jar was ever promoted to the live server, and only after a snapshot.
Jar-only promotion had one trap. The owners edited configs directly on the live server, so the plugin writes defaults only for keys that don’t exist yet. That makes a new setting safe to ship. But a changed value for an existing setting never reaches a running server; it has to be applied to the live config by hand, on staging and live both.
The plan for more players
Paper ticks each world on a single thread, so a bigger machine doesn’t make one busy world faster. On the two-vCPU box that put the ceiling at an estimated few dozen players. The plan for growing past it was Folia, a fork of Paper that splits the world into regions and ticks each one on its own thread. It was planned and never started.
The first step would have been moving the plugin’s 87 calls to the legacy scheduler onto Folia’s region, entity and async schedulers. Paper supports those too, so the work could have landed before the switch. The hard part is the generator tick: one global loop over every world isn’t allowed on Folia, so it would have had to become a task per region. EssentialsX, WorldGuard, Multiverse and Vault don’t run on Folia and would have needed replacing. The data layer would have needed no changes, because none of it ran on the game thread to begin with.