CI/CD for Minecraft Resource Packs
Tags: minecraft, devops
Published: 2020-09-22 16:51:18

I have recently been getting back into the world of modifying [Minecraft](minecraft.net) Java Edition, and as a part of that I have been spending some time learning the ins and outs of the new resource pack system.

Historically, within Minecraft, it was possible to create "texture packs", which would change out textures of blocks, items, mobs, and sounds. This lead to the creation of a fairly sizeable community, and provided a fantastic outlet for many creatives playing Minecraft. However, this approach was rather simplistic, with some clear limitations, which were later solved with the current iteration of "resource packs". Resource packs extend beyond switching out textures, allowing for the modification of the physical model of items and blocks, changes to in-game text, and replacing the music completely. Both of these systems draw their resources from either .zip files or directories with a particular structure.

While resource packs are associated much more closely with the creative side of Minecraft, I couldn't help but examine the technical possibilities regarding them. Since a large amount of metadata is stored as JSON files, specifically when it comes to custom models and defining alternative models or textures, it's easy to leverage version control systems such as Git to track changes. This is where the continuous integration and continuous eployment side of things comes into play. While developing a resource pack for a server, I realized quickly that deployment of changes was going to be difficult (for some context, Minecraft servers can define a custom resource pack to use, which clients can opt to download upon connecting), and started to investigate what could be done to make this process easier. The solution was relatively easy, but makes a huge impact on productivity and shipping new models or textures.

Step one was finding a place to host the finalized zip file, so that client connecting to the server can fetch and apply it. To this end, I opted for a free F1-micro instance on Google Cloud. The only potentially limiting factor in this case is the 1GB of egress traffic from the server, but given the small size that the resource pack is currently (~11kb, so roughly ~1,100,000 downloads if my math is correct), I can safely ignore this for the time being. To serve the files themselves, I installed nginx and grabbed a certificate using certbot and LetsEncrypt.

With the basic hosting out of the way, we can focus on the process of delivering the zip file. For this project, I'm using [sourcehut](https://sourcehut.org), and opted to use the available builds system for convenience (and to take a look at the competition!). There are three things to do to ensure the resource pack can be delivered:

1. Ensure the JSON resources are correctly formatted. Otherwise, models and textures will be broken. Unfortunately, checking the correctness of the data itself is another problem.

2. Package the resource pack as a zip file.

3. Upload this zip file to the server.

None of the steps above are necessarily "novel" when it comes to testing and delivering code, and was relatively simple to implement with the build manifest on sourcehut.

```yaml
image: alpine/edge
packages:
  - zip
  - rsync
  - jq
secrets:
  # Secrets go here.
sources:
  - git@git.sr.ht:~username/repository
environment:
  deploy: username@deployment-server
tasks:
  - test: |
      cd repository
      # Ensure all JSON files are readable JSON.
      find . -type f -name "*.json" -exec cat {} + | jq . > /dev/null
  - build: |
      cd repository
      zip -r resource-pack.zip assets pack.mcmeta
  - deploy: |
      cd repository
      rsync -e "ssh -o StrictHostKeyChecking=no -i $HOME/.ssh/private-ssh-key" -avz resource-pack.zip $deploy:/public-directory
```

Fantastic! End of story, right? It very well could be, but we can take this a step further. Minecraft servers can also specify a SHA-1 hash so clients can verify the integrity of the resource pack before applying it. While in most cases this is not an issue, it is helpful to specify.

Rather than creating a small webserver to run continuously on an already constrained machine, I opted instead to leverage nginx and a simple static page, which is modified during the deployment process with the new SHA-1. This way, we save on resources, and there are fewer moving parts over all. The `<code id="resource-pack.zip"></code>` portion will be replaced when we deploy the resource pack!

```html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Resource Pack Repository</title>
	<link rel="stylesheet" href="https://unpkg.com/mvp.css">
</head>
<body>
	<header>
		<h1>Resource Pack Repository <small>server-ready</small></h1>
	</header>
	<main>
		<h3>Resource Pack</h3>
		<p>A basic resource pack!</p>
		<p>Download: <a href="/packages/resource-pack.zip"><code>/packages/resource-pack.zip</code></a></p>
		<p>sha1sum: <code id="resource-pack.zip"></code></p>
	</main>
</body>
</html>

```

This `index.html` file is then deployed to our nginx server's web directory, to be served up when a visitor arrives at the website.

We can then add the following commands to the `deploy` step in our build manifest.

```shell
# Fetch the old index file.
curl http://deployment-server > index-old.html
# Update the code block with the new SHA-1
sed -r "s/(<code id=\"resource-pack\.zip\">).*(<\/code>)/\1$(resource-pack.zip | awk '{print $1}')\2/g" index-old.html > index.html
# Send the index file back up to the web host
rsync -e "ssh -o StrictHostKeyChecking=no -i $HOME/.ssh/private-ssh-key" -avz index.html $deploy:/public-web-directory
```

And voila! We now have a static site that will always have the most recent SHA-1 hash available.

![Final resource pack landing page](https://dev-to-uploads.s3.amazonaws.com/i/omd1mlcurr2yukgtdqri.png)

There are a few improvements that could be made here, such as minifying any JSON data before compressing, or testing the actual content to ensure it matches a schema, but given the small scope of this project, I'm happy with where it landed, and I can now focus on completing the rest of the resource pack. Feel free to ask any question in the discussion below!