Using Bazel to create Minecraft modpacks

An overview of how I automated the build process for CorePack

2020-10-24 bazel workflow git minecraft

All content of this post is based around the work I did here

Back in 2012, I got in to Minecraft mod development, and soon after, put together an almost-vanilla client-side modpack for myself that mainly contained rendering, UI, and quality-of-life tweaks. While this modpack never got published, or was even given a name, I kept maintaining it for years until I eventually stopped playing Minecraft just before the release of Minecraft 1.9 (in 2016). I had gotten so used to the features of this modpack, that playing truly vanilla Minecraft didn’t feel correct.

Recently, a few friends invited me to join their private Minecraft server, and despite having not touched the game for around four years, I decided to join. This was a bit of a mistake on their part, as they now get the pleasure of someone who used to main 1.6.4 constantly walking up to things and asking “What is this and how does it work?”. I have started to get used to the very weird new collection of blocks, completely reworked command system, over-complicated combat system, and a new rendering system that makes everything “look wrong”.

One major thing was still missing though, where was my modpack? I set out to rebuild my good old modpack (and finally give it a name, CorePack). Not much has changed, most of the same rendering and UI mods are back, along with the same GLSL shaders, and similar textures. Although, I did decide to take a “major step” and switch from the Forge Mod Loader to the Fabric Loader, since I prefer Fabric’s API.

Curseforge & Bazel

I don’t remember Curseforge existing back when I used to play regularly. It is a huge improvement over the PlanetMinecraft forums, as curse provides a clean way to access data about published Minecraft mods, and even has an API! Luckily, since I switched the modpack to Fabric, every mod I was looking for was available through curse (although, it seems NEI is a thing of the past).

My main goal for the updated version of CorePack was to design it in such a way I could make a CI pipeline generate new releases for me when mods are updated. This requires programmatically pulling information about mods, and their JAR files using a buildsystem script. Since this project involves working with a large amount of data from various external sources, I once-again chose to use Bazel, a buildsystem that excels at these kinds of projects.

While Curseforge provides a very easy to use API for working with mod data, @Wyn-Price (a fellow mod developer) has put together an amazing project called Curse Maven that I decided to use instead. Curse Maven is a serverless API that acts much like my Ultralight project. Any request for an artifact to Curse Maven will be redirected, and served from the Curseforge Maven server without the need for me to figure out the long-form artifact identifiers used internally by curse.

Curse Maven makes loading a mod (in this case, fabric-api) into Bazel as easy as:

# WORKSPACE
# Load bazel_maven_repository
http_archive(
    name = "maven_repository_rules",
    strip_prefix = "bazel_maven_repository-1.2.0",
    type = "zip",
    urls = ["https://github.com/square/bazel_maven_repository/archive/1.2.0.zip"],
)
load("@maven_repository_rules//maven:maven.bzl", "maven_repository_specification")
load("@maven_repository_rules//maven:jetifier.bzl", "jetifier_init")
jetifier_init()

# Declare any mods as maven artifacts
maven_repository_specification(
    name = "maven",
    artifacts = {
        "curse.maven:fabric-api:3049174": {"insecure": True}
    },
    repository_urls = [
        "https://www.cursemaven.com",
    ],
)

The above snippet uses a Bazel ruleset developed by Square, Inc. called bazel_maven_repository.

Modpack configuration

Since my pack is designed for use with MultiMC, two sets of configuration files are needed. The first set tells MultiMC which versions of LWJGL, Minecraft, and Fabric to use, and the second set are the in-game config files. Many of these files contain information that I would like to modify from Bazel during the modpack build step. Luckily, the Starlark core library comes with an action called expand_template. expand_template is basically a find-and-replace tool that will perform substitutions on files. Since this is an action, and not a rule, it must be wrapped with a small rule declaration:

# tools/template.bzl
def expand_template_impl(ctx):
    ctx.actions.expand_template(
        template = ctx.file.template,
        output = ctx.outputs.out,
        substitutions = {
            k: ctx.expand_location(v, ctx.attr.data)
            for k, v in ctx.attr.substitutions.items()
        },
        is_executable = ctx.attr.is_executable,
    )

expand_template = rule(
    implementation = expand_template_impl,
    attrs = {
        "template": attr.label(mandatory = True, allow_single_file = True),
        "substitutions": attr.string_dict(mandatory = True),
        "out": attr.output(mandatory = True),
        "is_executable": attr.bool(default = False, mandatory = False),
        "data": attr.label_list(allow_files = True),
    },
)

In a BUILD file, template rules can be defined as follows:

# BUILD
load("//tools:template.bzl", "expand_template")

expand_template(
    name = "my_config",
    template = "config.json.in",
    out = "config.json",
    substitutions = {
        "TEST_SUBS": "hello world"
    }
)

Using the following example file as config.json.in, this rule would have the following effect:

// config.json.in
{
    "key": "TEST_SUBS"
}

// config.json
{
    "key": "hello world"
}

Packaging

Once mods are loaded, and configuration files are defined in the buildsystem, I use a large number of filegroup and genrule rules to set up a directory hierarchy in the workspace, and wrap everything in a call to zipper to package the modpack into a ZIP file.

Finally, I use GitHub Actions to automatically run the buildscript, and publish the resulting MultiMC instance zip to the GitHub repo for this project.



Thank you for reading this post. If you enjoyed the content, and want to let me know, or want to ask any questions, please contact me via one of the methods listed here. If you would like to be notified about future posts, feel free to load my rss feed into your favorite feed reader, or follow me on Twitter for notifications about my work and future posts.

If you have the time to read some more, I recommend checking out one of the following posts:

Connecting to a Minecraft server over IRC
This post outlines the process of writing a custom IRC server that can bridge between your favorite IRC client, and any Minecraft server
My first mechanical keyboard: The Vortex Core
I recently purchased my first mechanical keyboard, and decided to go "all in" with a 40% layout.
Mounting Google Drive accounts as network drives
I can never get the Google Drive webapp to load quickly when I need it to. My solution: use some command-line magic to mount my drives directly to my laptop's filesystem.


Made with ♥ by Evan Pratten | RSS | API Status