How do Neoforge installers work

My motivations are not entirely altruistic: through Voldeloom I learned that installing Forge 1.4-1.7 “like an end-user”, instead of decompiling the game, is a practical and realistic way to get a simple modding moolchain going. Surely nothing has changed in ten years, right.

todo, peep this that i found while looking for the installer

we’re gonna look at net.neoforged:neoforge:21.5.75:installer, aka this jar. It’s for 1.21.5

The main class is net.minecraftforge.installer.SimpleInstaller. If you pass --install-client or --install-server it will do that, otherwise if you’re in a headless environment it will install the server, otherwise it will pop the GUI. There’s also options to generate a “fat installer” with --fat-installer, with options --fat-include-minecraft, --fat-include-minecraft-libs, --fat-include-installer-libs, and --fat-offline. Could be neat.

It will attempt to install Let’s Encrypt root certificates. They are bundled in the root of the jar in the file lekeystore.jks with password supersecretpassword. Lol.

“install specs”

Installation specs are loaded from the install_profile.json file in the root of the jar. If spec is 0 you get an Install, otherwise you get an InstallV1. These are the instructions for how to install the jar / things to display in the GUI / things to copy into the vanilla launcher.

A DataFile has one key for client and another for server. It doesn’t have to actually be a file path. Data files in this installer are MAPPINGS, MOJMAPS, MERGED_MAPPINGS, BINPATCH, MC_UNPACKED, MC_SLIM, MC_EXTRA, MC_SRG, PATCHED, and MCP_VERSION.

A Processor is a description of a JVM invocation. A Processor optionally contains a List<String> sides (“client” or “server”), an Artifact of the jar to run, a List<Artifact> of the jars to stick on the classpath, String[] args, and a Map<String, String> outputs which appears to be unused. If the sides is missing, the processor runs when installing any side.

No maven-style artifact resolution is attempted. The locations of all needed libraries are specified in libraries complete with a URL.

A Mirror is a String name, image, homepage, url and boolean advertised. If the mirror is advertised, a “Data kindly mirrored by name at homepage” message will be displayed.

Action

Assuming the headless installer since the GUI doesn’t interest me. After the install profile is parsed (Util.loadInstallProfile called from SimpleInstaller#main), the Action is invoked. The action was selected from the command line, so it can be Actions.CLIENT, Actions.SERVER, or Actions.FAT_INSTALLER.

Fat installer

Makes a copy of the installer jar which includes the following goodies inside the zip:

If all three --fat-include options were passed, the installer copy also gets Offline: true set in MANIFEST.MF, which makes it act like --offline was passed all the time.

All the “download something” methods check the contents of the maven/ folder inside the jar before trying to contact the internet.

Server installer

Okay, now getting into actual minecraft stuff. Handled by the ServerInstall class.

We have a File target which is the directory to install the server into, sometimes called the “root”.

Client installer

Here the “target dir” is your vanilla launcher .minecraft directory expected to contain a launcher_profiles.json or launcher_profiles_microsoft_store.json file.

Tl;dr doesn’t do anything fancy like downloading assets, the vanilla launcher can handle that, and the real meat is handled by the processor system

Processor system

Handled in PostProcessors

modifying the datamap

First the install manifest’s data is modified. Here is a datamap excerpt to illustrate.

"MOJMAPS": {
  "client": "[net.minecraft:client:1.21.5-20250325.162830:mappings@txt]",
  "server": "[net.minecraft:server:1.21.5-20250325.162830:mappings@txt]"
},
"BINPATCH": {
  "client": "/data/client.lzma",
  "server": "/data/server.lzma"
},
"MCP_VERSION": {
  "client": "'1.21.5-20250325.162830'",
  "server": "'1.21.5-20250325.162830'"
}

At install time this would get collapsed to a simple Map<String, String> depending on whether the client or server is being installed. Then:

In practice: the only single-quote entry is the mcp version, the only extracted entry is the data/<side>.lzma file, and everything else is maven coordinates. I think most of the maven coordinates do not refer to real files either and they are created by further processors.

Finally the datamap is amended with a few extra entries:

These go alongside MAPPINGS, MOJMAPS, MERGED_MAPPINGS, BINPATCH, MC_UNPACKED, MC_SLIM, MC_EXTRA, MC_SRG, PATCHED, and MCP_VERSION from the installer manifest. Any argument to a processor invocation can substitute any of these variables.

processor driver loop

Just for reference, a sample processor json

{
  "sides": [
    "server"
  ],
  "jar": "net.neoforged.installertools:jarsplitter:2.1.2",
  "classpath": [
    "net.neoforged.installertools:jarsplitter:2.1.2",
    "net.sf.jopt-simple:jopt-simple:5.0.4",
    "net.neoforged:srgutils:1.0.0",
    "net.neoforged.installertools:cli-utils:2.1.2"
  ],
  "args": [
    "--input",
    "{MC_UNPACKED}",
    "--slim",
    "{MC_SLIM}",
    "--extra",
    "{MC_EXTRA}",
    "--srg",
    "{MERGED_MAPPINGS}"
  ]
},

the actual processors

Looking back at the installer manifest now. These run top-to-bottom. I’ll need to look at these closer. I have not reproduced all invocation arguments because there are a zillion

EXTRACT_FILES is a little strange, really feels like the installer should do that itself. Because it’s done with an external tool, there is no way to perform variable substitution in the actual launch scripts, so launch scripts refer to libraries/net/minecraft/server/20250325.162830/server-20250325.162830-extra.jar by name instead of {MC_EXTRA}. Why bother with variable substitution if you need to hardcode things anyway… TODO, find their release engineering for installer jars.

Jarsplitter means that ART and binarypatcher don’t have to carry dead weight like images and json files, or remap classes that don’t need remapping. The -extra jar does indeed end up on the classpath. Neat

Since binpatches are done as a last step, if you were to replace parts of this toolchain you have to make sure to do everything exactly the same as neo does, and produce byte-for-byte identical classes. Fortunately the installer manifest pins the exact version and classpath of every processor that touches the minecraft jar.

And that’s the end of the installer

tada