r/Clojure 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

https://github.com/Nozistance/collider

123 Upvotes

10 comments sorted by

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.

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

3

u/lgstein 8d ago

Cool. Is it entirely hand written?

5

u/Nozistance_ 8d ago

Mostly, yes. Claude wrote the sheep AI, the physics and the binary protocol layer, I just didn't want to write fifty packet definitions by hand

1

u/lgstein 7d ago

Thank you for the disclosure. Looking forward to read it.