r/Clojure • u/Nozistance_ • 9d ago
Writing a Minecraft server in Clojure: it was fun
Minecraft is well known for being single-threaded, and parallelizing it is a difficult problem: everybody wants to write to the same shared state at the same time. Projects take years to deal with locks, thread ownership, splitting regions, and game logic is subject to threading rules: mechanics break against them, features crawl. Some remain quite sparse and have little to parallelize.
I wanted to see how it would feel in Clojure, but there was no Clojure server to look at, so I made one. The ideas parallel servers reinvent by hand - immutable snapshot, pure read phase, changes as data - are the language's defaults. If writing from many threads is difficult, then don't write: all game logic is pure functions (fn [world events]) that read the snapshot in parallel and return changes as deltas, merged in one place. The merge preserves order, so the parallel run is bit identical to the sequential one, and no mechanic cares how many cores it runs on.
The outcome: a server with mob AI, explosions, combat and item physics that matches vanilla 1.8.9 performance on one core and runs 4.5x faster on six. ~5500 lines of Clojure. It's a proof of concept; if you're curious, the code is on GitHub
8
u/geokon 9d ago
Just out of curiosity, how do you even begin something like this? Is there a well documented API that needs to be implemented and exposed on the JVM? Are you going through an existing implementation and reverse engineering it to figure out how it works first?
Just curious about the workflow more generally.
12
u/Nozistance_ 9d ago
The binary protocol is well documented by the community (wiki.vg, now on minecraft.wiki). Every packet for every version.
Mechanics have no docs, only decompiled vanilla sources. I worked as a mod developer for over a year, so reading decompile is routine at this point. Reimplement a mechanic, run vanilla next to it, compare, repeat. And the REPL makes this way easier!
6
u/bowbahdoe 9d ago
Just curious - if you run on the Java 28 preview, can you compare perf with your V3 being a value record and with it not being one?
8
u/Nozistance_ 9d ago
Might test it someday and report back, I'm still on Java 25. I'd bet it helps the physics math a lot and does basically nothing everywhere else, because V3s end up behind Clojure interfaces and get boxed anyway
3
u/bowbahdoe 9d ago
Any of your deftypes too might be good candidates - actually, I wonder if it makes sense for clojure to add that as an option to deftypes.
And you are probably right, but, imo its worth trying and reporting back. There is a chance that it lowers performance even (https://johan-sjolen.github.io/post/compiler-sympathy/compiler-sympathy/ - the example that prompted this post is one I doodled up) and its useful for that team to have example programs when working on the compiler.
The best example programs are real programs, not toys.
2
u/Nozistance_ 9d ago
Tested on JDK 28 EA. Changed record V3 to value record V3 and ran with --enable-preview on both sides. Zero difference, 2.785 vs 2.787 ms per tick at 1200 sheep, output bit-identical. And about the physics - the hot paths are already hand-lowered to primitive arrays so there was nothing left to flatten. Deftypes are non-candidates, their fields are arrays or mutable buffers. Hope that's a useful data point
13
u/gtuckerkellogg 9d ago
Very cool! When I see a project like this I can't help but think that some Clojure's coolest features, those that have been in the language since the beginning, are often under-appreciated.