My son loves Minecraft. (Witness the evidence from his YouTube channel.) He even loves the music of Minecraft, particularly the music during the credits in the 1.13 version; and will sometimes put on the credits to listen. We wanted to be able to play this music at his upcoming birthday party, without having to bring a computer and have Minecraft running.
Figure 1: This is the two of us playing the Minecraft Mini-game "Raft Clash"
My wife found a tutorial for extracting the music, but we couldn't get
the example code to run on our Macs. However, I could understand what
it was trying to do sufficiently to write my own version. I decided to
write it in Python, because parsing JSON with grep
offends my
sensibilities1. I'm not a Python expert though, so please don't use this
as an example of how you should write Python!
Through this I learnt about how Minecraft stores its assets. The core
idea (for our purpose, anyway) is that each Minecraft version has an
index file containing a list of all its assets, and their SHA1
hash
and their size. Here is an example:
{ "objects": { "icons/icon_16x16.png": { "hash": "bdf48ef6b5d0d23bbb02e17d04865216179f510a", "size": 3665 }, "minecraft/sounds/music/game/water/axolotl.ogg": { "hash": "ee92e4ed79b3c4c47eabe71b36375b5d3f05b017", "size": 10423856 } } }
These hashes attached to the asset names corresponds to file names in
the objects
directory, which is the asset we want. The names are the
SHA1 hash of their contents. This is pretty neat, and allows Minecraft
to store assets for multiple versions efficiently. If an asset changes
from one version to the next, only the checksum in that version's
index file needs to change, and the corresponding object added to the
objects
directory.
We wanted music from the most recent Minecraft version. To avoid having to edit the script (or make it take arguments) whenever we install an update, I decided to make the script automatically find the most recent version. It does that by listing all the indices files (line 11), sort the list, and take the most recent (line 12).
To play it safe, we wanted to be able to run the script multiple times
without things breaking or overwriting previously extracted music. To
do this I put the music files in a versioned directory under
~/Music/Minecraft
. I removed the file extension (i.e. the .json
bit)
from the index filename, and used that as the version number (see line
15).
We were only interested in music, and like the existing script only
extracted all the assets with "music" in their name. We also wanted to
keep the context provided by the full asset name. (It's not
immediately obvious to me that "axolotl" has to do with water.) This
means I had to create the full directory path to the extracted file,
however, which I do at line 34. We have to check if the
directory already exists first, otherwise the os.makedirs
function
throws an exception2.
The find
(line 22) function3 iterates over all the files and directories under the
directory in its first argument, and returns the full path if it finds
a file whose name matches the second argument. In our case we pass it
the assets directory and the object hash, see line 29.
The full code is below.
1: #!/usr/bin/env python 2: import os 3: import json 4: from shutil import copyfile 5: 6: # This is where vanilla Minecraft stores its assets 7: assets_path = os.path.expanduser( 8: "~/Library/Application Support/minecraft/assets") 9: 10: indices_dir = os.path.join(assets_path, "indexes") 11: indices = os.listdir(indices_dir) 12: index_file = sorted(indices)[-1] 13: index_path = os.path.join(indices_dir, index_file) 14: 15: version, _ = os.path.splitext(index_file) 16: output_dir = os.path.expanduser( 17: "~/Music/Minecraft/%s/" % version) 18: 19: with open(index_path, "r") as read_file: 20: objects = json.load(read_file)["objects"] 21: 22: def find(name, path): 23: for root, dirs, files in os.walk(path): 24: if name in files: 25: os.path.join(root, name) 26: 27: for k in objects: 28: if "music" in k: 29: asset_path = find(objects[k]["hash"], assets_path) 30: outfile = os.path.join(output_dir, k) 31: print("Extracting %s..." % outfile) 32: 33: if not os.path.exists(os.path.dirname(outfile)): 34: os.makedirs(os.path.dirname(outfile)) 35: copyfile(asset_path, outfile)
There you have it! You can use this on MacOS to extract the music from Vanilla Minecraft4. One improvement I considered, but ultimately decided was not needed (yet) was the ability to specify which version to extract the music from, and the directory to extract the music to.