HorizonEvents: From Datapacks to Coroutines — Building a Kotlin-Powered Minigame Plugin
2025-06-17 • ~6 min read
Four years ago I (ddan) kicked off the HorizonEvents minigame project with nothing more than command blocks and datapacks. That solo, scrappy approach let me publish fast, gather feedback, and avoid the rabbit-hole of plugin APIs. Back then the minigames were hosted on a small Apex Minecraft Hosting server and were bridged into the Hallowed Survival lobby so friends could hop in without switching servers. The code still lives on rented hardware at Apex—soon migrating to Pebble Hosting and maybe one day a VPS—but today that instance is federated with the Horizon Ventures network—same autonomy, far bigger audience. As the project expanded I pulled in an awesome team of builders to sculpt maps and aesthetics, while I stayed laser-focused on the code. But once our ideas—and the player counts—grew, those lovingly hacked-together datapacks began to creak. In late 2024 we hit the ceiling and decided to rewrite everything as a Kotlin plugin for Paper. The result is HorizonEvents, a data-driven, hot-reloadable framework that powers Freeze Tag, Endless Maze, Capture the Flag, Spleef, and every new idea we throw at it.
That first release—version 1.0—was very much a learning experiment. It works but only supports a single server and lobby, so a big influx of players would bottleneck things. I'm rewriting it now as version 2.0 with multi-server support to eliminate those lag issues and add every feature I've dreamed of.
Why We Started with Datapacks
- Zero install friction. Players join vanilla 1.21 and everything just works.
- Ridiculously fast iteration. A one-liner in chat reloads the pack; testers are back in seconds.
- Future-proof. Datapacks ride along with every Mojang update.
The trade-off? Logic is limited to scoreboard math, summon/fill loops, and the mercy of /execute
. Once a game needed timers, state machines, or network-wide events, the command spaghetti turned into a Hydra.
When the Ceiling Hit
By mid-2024 we were juggling 20+ game modes. Each new feature required:
- Copy-pasting scoreboard templates.
- Adding yet another
tick.mcfunction
check. - Hoping nothing clashed with the
counter
entity IDs.
Performance dipped, and debugging via tellraw
felt medieval. Fun fact: one freeze-tag bug went unnoticed for two weeks because its scoreboard name was one character off.
Meet HorizonEvents — The Events Minigame Plugin
Migrating to Kotlin + Paper wasn’t just a port; it was a redesign around three principles:
- Data first. Every map ships with a
MapConfig.json
. An extension function converts it to a runtimeGameMap
object, so designers tweak JSON—no recompiles. - Explicit lifecycles.
GameManagerImpl
owns a cleanstart → play → end → cleanup
flow. Each game extends an abstractGame
base, overriding only what’s unique. - Hot-reload everything.
/reloadhorizonevents
tears down async tasks, closes inventories, flushes caches, and spins the world back up—all without kicking players.
What Kotlin Unlocks
Datapack World | HorizonEvents World |
---|---|
Scoreboard counters & chat spam | Strongly-typed events + Adventure API UI |
One long tick function |
Coroutine-driven schedulers |
raycasting via armor stands |
Real hit detection through Paper events |
Manual world resets | Snapshot -> copy -> delete via MapManager |
Concrete wins:
- Coroutines run timers without blocking the main thread.
- Adventure components give us hover-text NPC nameplates and RGB hex chat.
- Kotlin DSLs shrink boilerplate for items, commands, and listeners.
- SQLite + async profiles enable persistent cosmetics without TPS drops.
Case Study — Freeze Tag Reborn
Old datapack version:
- ~600 lines of functions.
- Freeze/unfreeze via armor-stand tags.
- World reset required a manual
/fill
of the arena.
HorizonEvents version:
- 120 lines of Kotlin.
- Real player metadata + MythicMobs power-ups (grow/shrink mushrooms!).
MapManager.cloneWorld()
ensures a fresh arena every round.
Result: 38% less CPU time and zero ghost-tag bugs.
Lessons We Learned (So You Don’t Have To)
- Prototype in datapacks, but plan escape hatches early—namespacing, modular resets, etc.
- Treat configs as content, not code. JSON beats recompiles.
- Own your reload path. A live server demands clean teardown logic.
- Leverage Kotlin’s strengths—null-safety, coroutines, and DSLs tame Bukkit’s rough edges.
Ready to Level Up?
If you’re hacking together your first minigame, start with a datapack—shipping fast trumps elegance. But when players start asking for leaderboards, cosmetics, or 100 concurrent zombies, consider the jump. HorizonEvents proves that a Kotlin-powered plugin can keep the spirit of rapid iteration and deliver the muscle for serious scale.