Back to projects

Rebuilding Lilies in Java

Lilies is a factory-and-prestige Minecraft server. The first version was written in Skript and reached 90 players online. For Season 1 I am porting every system to a modular Java plugin suite, one feature at a time.

Role
Owner and sole developer
Stack
Java 21, Paper 1.21.11, Gradle, SQLite, PacketEvents
Status
Season 1 in development
The Lilies tab list showing 90 players online

Why rewrite something that worked

Skript let me ship fast. By the end of the first season the server ran on about 57 script files, around 816 KB of code, and it held 90 players at once. It also had limits I could not fix from inside Skript: no real types, no tests, shared global variables for everything, and every system reaching into every other.

Before porting anything I audited the old code line by line. The ledger came to 1,038 rows. The worst find was the conveyor paying out from two separate loops, so some players earned about ten times what the economy was balanced for. There were item dupes too. Every row became either a fix in the new code or a test that proves the bug cannot come back.

The shape of it

The new build is one core plugin and a jar per feature. The core owns the things every feature needs, and each feature is its own small plugin that can be built, tested and turned off on its own.

The Paper server loads LiliesCore, which loads a jar for each feature Paper serverMinecraft 1.21.11 LiliesCoreloader, data, config Economy jarsgens, conveyors, furnaces Social jarsgangs, trading, crates Events jar14 minigames
  • lilies-api holds interfaces only. Features talk to each other through Bukkit's ServicesManager against those interfaces, never by reaching into each other's code.
  • LiliesCore finds the jars in its features folder and loads them through Paper's own plugin manager, in dependency order.
  • Feature jars are about 30 modules in all. Each one ships its own config, messages, items and menus.

Everything is a config file

Prices, drop rates, menu layouts and every line of chat text live in YAML, one folder per module. Staff can run /lilies reload for everything or a single module, and the new values are checked before they replace the old ones, so a typo leaves the running config in place. Small values can be changed in game with /lilies set without opening a file.

Fast with a full server

  • SQLite in WAL mode with a single async writer thread, so the main game thread never waits on the disk. Player profiles are cached in memory.
  • Holograms and moving items are packet-only displays that the client interpolates, instead of entities teleported every tick.
  • One shared per-second scheduler instead of a timer per system.

A load test with 50 simulated players harvesting 20 crops a second each costs 0.37 ms per tick. The server has 50 ms per tick to spend, and it held a steady 20 TPS.

Tests that caught real bugs

The suite had 501 tests once the core was done and 780 by the end of the money-loop phase. The most useful test was not a unit test at all. A boot test starts a real server with every jar loaded, and it caught a bug the unit tests could not see.

When a player right-clicks air, Paper fires the interact event already marked as cancelled. Seven handlers were set to skip cancelled events, so those features quietly ignored every air click. Every mock in the unit tests built the event uncancelled, so they all passed. The boot test used the real server and failed straight away.

Events without lost items

The events module runs 14 minigames. Joining one snapshots the player's inventory and state. Every way out of an event, whether winning, losing or leaving, restores that snapshot first. Prizes are paid only after the restore has been saved, so a crash in between cannot hand out a prize and wipe an inventory, or the other way round.

What I took from it

  • Auditing before porting meant the new code fixed the old bugs instead of copying them.
  • Mocks agree with your assumptions. A test against the real server is the one that argues back.
  • Putting every number in config means balancing the economy stops needing a developer.