This is a pure Common Lisp library for editing binary files generated by Minecraft. It is a work in progress, so expect minimal error-checking, few optimizations, and so on. Back up your Minecraft save files before use.
Once the low-level stuff is completely done, the idea is to create a higher-level DSL for manipulating Minecraft worlds straight from the REPL. Think NBTExplorer and MCEdit filters rolled into one, but with the flexibility of Common Lisp.
I have already used this library to implement something akin to the Minecraft Land Generator (which I will share soon) and there are naturally many more potential uses.
As yet, both compressed and uncompressed .dat
files, whose contents consist
entirely of a single Named Binary Tag structure - see
here - are supported. Support for
region (.mca
) files is partially implemented, but there are some technical
challenges posed by this that I have yet to overcome.
With Quicklisp it's trivial:
$ cd ~/quicklisp/local-projects/
$ git clone https://github.com/pnoom/cl-nbt.git
Start up SLIME and run:
CL-USER> (ql:quickload :cl-nbt)
The API is not set in stone (hence the lack of exported symbols - just use
(in-package :cl-nbt)
for now), but the functions should be pretty
self-explanatory. Read a .dat
file from your Minecraft world's save directory
like so:
CL-USER> (defparameter level-dat
(read-dat-file "~/.minecraft/saves/my-world/level.dat"))
==> LEVEL-DAT
Access a nested tag by providing a list of names/indexes of the tags that make up its "path", eg:
CL-USER> (get-tag level-dat "Data" "RandomSeed")
==> #<TAG-LONG "RandomSeed">
CL-USER> (payload *)
==> -981234193418740174
CL-USER> (setf (payload (get-tag level-dat
"Data" "GameRules" "naturalRegeneration"))
"false")
==> "false"
CL-USER> (write-dat-file "~/.minecraft/saves/my-world/level.dat" level-dat)
==> T
The above first returns the value of the world's seed number, then disables player health regeneration, then writes the changes back out in-place.
Any feedback or contributions would be very welcome. Beyond docstrings and the odd comment there's no documentation yet, but the code is quite simple, and the NBT spec will clear up any confusion. Some things to note:
-
I adapted a couple of functions from the
binary-types
library, but wrote the rest of the binary I/O stuff from scratch, because at the time, I couldn't actually figure out how to use the library. My code should be pretty easy to follow though. -
AFAIK the compression ratio of data compressed by the
salza2
library (see here) is lower than that of the data compressed by Minecraft, so the files written bycl-nbt
may be slightly larger than the originals. There's no way to change the ratio at the moment, unfortunately. For.dat
files this is no problem, but it may prove problematic for.mca
files. -
I haven't yet specialized the arrays that form the payload of
tag-byte-array
andtag-int-array
, so reading a file with many such tags uses a lot more memory than it should. To make matters worse,.mca
files can (so far) only be read and written all at once (as opposed to being able to read chunks from the file individually) so if they're over a couple of MB in size, your Lisp may keel over(!) You could always assign more dynamic space to your Lisp on startup, but that shouldn't be necessary once the code has basic space optimizations. -
Comparing the data structure produced by
read-mca-file
and NBTExplorer for the same.mca
file, the order of the chunk entries seems to differ: in one, the chunks are ordered by increasing (x,z), while in the other it's (z,x). Writing it out withwrite-dat-file
and reading it in again changes the order once more, so one or both of those functions does not preserve the order of the entries, right? But I can't see how I've deviated from the spec...
Happy hacking, mining and crafting! :)