Skip to main content
Unlike regular mods, Essential Loader is subdivided not by Minecraft version but by which host mod loader it targets. This is because Essential Loader does not interact with Minecraft directly—it heavily interacts with the host mod loader to dynamically load mods.

Supported Platforms

Essential Loader currently supports four different platforms:

LaunchWrapper

Forge 1.8.9 and 1.12.2

Fabric

1.16+ with Fabric Loader

ModLauncher 8

Forge 1.16.5

ModLauncher 9

Forge 1.17+ (also supports ModLauncher 10)

LaunchWrapper

LaunchWrapper is what Forge 1.8.9 - 1.12.2 are based on.

Background

Technically LaunchWrapper is independent from Forge (it is published by Mojang), so:
  • Stage0 is completely independent from Forge
  • Stage1 has separate code paths for pure LaunchWrapper vs LaunchWrapper+Forge
  • In practice, Forge is the only big user of LaunchWrapper, so stage2 assumes Forge
Conceptually, LaunchWrapper itself is quite simple (basically just three medium-length files). This makes it fairly easy to pick up but relatively hard to master because there aren’t any mechanisms for dependency management or hard rules for what a Tweaker may or may not do.
Rules are implicit in exactly how LaunchWrapper is implemented and are usually only discovered after something breaks in some obscure configuration of mods.

Entrypoint

LaunchWrapper’s main entry point is an ITweaker class. Discovery process:
  1. The Launch class discovers and invokes tweakers in multiple rounds
  2. Each round can add extra tweaker classes for the next round
  3. Round 1: Tweakers discovered via command line arguments (includes only Forge in production)
  4. Forge: Looks at the TweakClass property in MANIFEST.MF of jars in the mods folder during early loading (CoreModManager)
  5. Round 2: This is where Essential Loader gets to run
In hindsight, we could have used Forge’s CoreMod mechanism which runs during the first round. However, a Tweaker has worked well enough and changing this now would be a huge undertaking.
Chain-loading mods: Given this is all LaunchWrapper land and happens before Forge proper starts loading regular mods, chain-loading production mods is almost trivial: just add them to the classpath and Forge will discover them as usual.
In development environments, the entrypoint is somewhat different because the tweaker is enabled via the command line. This is compensated for mostly by DelayedStage0Tweaker.

Hacks

Despite its conceptual simplicity, the stage2 loader for LaunchWrapper has grown to be arguably the most complex piece of essential-loader. This is due to:
  • The amount of workarounds necessary to run modern software (such as Mixin 0.8.x) on old Minecraft versions
  • Badly written Tweakers/CoreMods in third-party code that make everyone’s lives more difficult

Relaunch

The most powerful tool available on LaunchWrapper is the “relaunch”.
A relaunch creates a second, mostly clean, mostly isolated environment within the existing LaunchWrapper environment and re-launches Minecraft in there. This allows:
  • More recent versions of certain libraries
  • Modifications to the inner LaunchWrapper that would otherwise not be possible
When is relaunching required?
  • 1.8.9: Always required (default ASM library is too old for Mixin 0.8)
  • 1.12.2: Could work without relaunching if no other mod pulls in an older version of our libs (Kotlin being a frequent example), but for simplicity we always relaunch

Other Hacks

There are numerous other hacks used on this platform. For now, refer to the source code for details.

Fabric

The Fabric platform is used wherever fabric-loader is used, regardless of Minecraft version.

Background

With fabric-loader working across the entire range of supported Minecraft versions, the Fabric variant of essential-loader naturally does as well. Advantages:
  • Not stuck with old software like unsupported Forge versions
  • Excellent built-in support for:
    • Picking the latest version of a mod
    • Bundling mods inside other mods (Jar-in-Jar, JiJ)
    • First-party Mixin support
Limitations:
  • Can’t snoop around in internals as much (updates might break our hacks)
  • No way to chain-load mods or have custom code do mod discovery
  • Always discovers all mods before running any third-party code
Unlike LaunchWrapper, fabric-loader was written to be a mod loader (take note ModLauncher!) and has excellent built-in functionality.

Entrypoint

We use the built-in prelaunch entrypoint for essential-loader.
{
  "entrypoints": {
    "prelaunch": [
      "gg.essential.loader.stage0.EssentialLoader"
    ]
  }
}

Hacks

There are only three bigger hacks required on the Fabric platform, all related to chain-loading the mod:
  1. Adding it to the class loader
  2. Registering it as a mod for other mods to see
  3. Extracting JiJ mods

Fake Mod

We make heavy use of fabric-loader internals to instantiate mod metadata for dynamically loaded mods so they appear as normal mods in ModMenu.
Because this makes heavy use of internals, it’s particularly likely to break with fabric-loader updates. We’ve tried to keep it optional—if it fails, the mod will generally still function correctly, just won’t show up in ModMenu.
Important caveat: Declared entrypoints will only be registered if this succeeds. If possible, mods should not rely on them (e.g., Essential uses a custom mixin for its init entrypoint instead of the fabric built-in entrypoint).

Jar-in-Jar (JiJ)

By the time we run, fabric-loader has already loaded all mods. We need to handle any JiJ mods inside our dynamically loaded mods ourselves. Simple cases:
  • Not yet loaded: Trivial to handle
  • Newer version already loaded: Trivial to handle
Complex case: Older version already loaded If an older version of a JiJ mod has already been loaded, we can’t easily replace it with the newer version. Solution:
  1. Essential dynamically generates a new “Essential Dependencies” mod in the mods folder
  2. Prompts the user to restart the game
  3. On next boot, fabric-loader sees the generated jar and loads the updated version
This unfortunately means the user may need to manually restart on each update of the JiJ mod. However, this solution doesn’t rely on fabric-loader internals at all!

ModLauncher 8

ModLauncher 8 is used by Forge 1.16.5.

Background

ModLauncher is Forge’s successor to LaunchWrapper. It has much bigger ambitions and is much more complex.
Unfortunately for us, most of what it does is geared specifically to Forge and is of little use to us. So in the end, it’s usually just much more difficult to get it to do what we want.
Consequently, we more often need to resort to hacks, and those hacks are usually more complex.

Entrypoint

The early entrypoint which ModLauncher provides is the TransformationService. Discovery:
  • Discovered by Forge from jars in the mods folder
  • Uses a file as used by Java’s ServiceLoader (rather than a manifest entry)
META-INF/services/cpw.mods.modlauncher.api.ITransformationService
Multi-stage forwarding: To maintain maximum flexibility:
  • Stage1 has its own TransformationService (can be updated)
  • Stage0 forwards all methods to stage1
  • Stage1 forwards all methods to stage2 (can auto-update)
ModLauncher 9+ requirement: Does not allow two jars to share any packages. Anyone packaging stage0 must relocate it.Also requires each TransformationService to have a unique name. Because each must have a unique package, all transformation services (including stage2) must handle being instantiated multiple times and return a unique name for each.
Chain-loading: Unlike LaunchWrapper, chain-loading requires registering a ModLocator. It’s a bit of a dance because it gets loaded in a separate class loader that cannot directly communicate with the TransformationService, but otherwise fairly straightforward.

Hacks

Upgrading Third-Party Mods

Despite ModLauncher being a “Mod Loader”, it fails to do the most basic thing: provide a method to load the newer of two mods. Why we need this: On Forge, Kotlin is part of a mod called KotlinForForge (KFF), and people frequently have an old version installed. Solution: We Unsafe-clone the LanguageLoadingProvider and replace it with a different implementation (SortedLanguageLoadingProvider) that:
  • Sorts jars by implementation version
  • Only picks the most recent one where there are multiple jars for the same implementation name
Forge has a completely different mechanism for loading language mods vs regular mods. We only deal with the language one right now because we don’t yet need to upgrade regular ones.

Mod/Language Load Order

Modern Forge differentiates between regular Mods and Language Mods. The problem: Classes in regular mods take priority over language mods. So even after we upgrade KotlinForForge, if another mod bundles the Kotlin stdlib, we’ll get their (usually outdated) version. Solution: We mess with the Lambda used to fetch bytes for a given class name. This must be done before any classes are loaded, which is why we “register” (using Unsafe) an EssentialLaunchPluginService which has a method invoked at the right time.

Upgrading Kotlin

KotlinForForge sometimes:
  • Takes a while to update
  • Does backwards incompatible changes (might break third-party mods)
  • No longer updates at all for certain versions (anything that isn’t Forge LTS)
Solution: Upgrade Kotlin independently from KotlinForForge. Language mods still work similarly to LaunchWrapper (basically all pushed into a single URLClassLoader), so it’s fairly easy to push the Kotlin stdlib into the class loader before KotlinForForge gets loaded.

ModLauncher 9

ModLauncher 9 is used by Forge 1.17+. ModLauncher 10 is fairly similar and supported by the same platform code.

Background

The main difference from ModLauncher 8 is that it heavily uses Java 9’s module system, which changes pretty much everything.
ModLauncher 10 is fairly similar to ModLauncher 9, so it is supported by the ModLauncher 9 variant of essential-loader. There’s an inner modlauncher10 project for the one bit that is different, bundled inside the modlauncher9 stage2 jar.

Entrypoint

See the ModLauncher 8 section above—the entrypoint is the same. Why we can’t reuse stage0/1 from ML8: The TransformationService interface now references a record class, which we can’t compile against with Java 8 required by ML8.

Hacks

While LaunchWrapper has the most hacks, ModLauncher 9 isn’t too far off. And with ModLauncher generally being more complex, the hacks are too.
Additionally, since ModLauncher 9 uses Java 9’s modules, we often can’t access even public members via reflection. Not only do we need to mess with ModLauncher internals, we also need to rely on Unsafe a lot to even access those.

Chain Loading Mods

You’d think this would be easier now that language and regular mods are more uniform. But no. The problem:
  • We still need to implement our own ModLocator
  • There’s no longer any way to actually register it
  • Forge won’t call it for us
Solution: We have to call it ourselves and inject the results into Forge at the correct moment in time. However, we can’t know that moment for sure ahead of time because it depends on HashMap iteration order, so we try at multiple points.

What Minecraft Version Is This Anyway?

Seems like a simple question, but you won’t get an answer from ModLauncher (even though it technically has the info). Solution: Wait until the FMLLoader has run to make the Minecraft version publicly available. Until then:
  • The EssentialLoader implementation in stage2 accessed by stage1 is a dummy
  • Gets a hard-coded 1.17.1 as the Minecraft version
  • Only provides access to the stage2 TransformationService
Once we know the version, we instantiate ActualEssentialLoader which checks for updates and downloads the initial version.

Upgrading Third-Party Mods

Not much has changed on a high level from ModLauncher 8. ModLauncher still fails to load the more recent of two versions. What has changed internally:
  • Instead of file system order → HashMap iteration order (effectively random on each run)
  • Java’s module system throws an exception when two modules have overlapping packages
  • Luckily, if the same module is defined by two jars, it picks one without exploding
Positive note: “Language mods” and “regular mods” are now both handled fairly similarly in two different ModLauncher layers. Once we fix one, we can fairly easily get the other for free. Solution: SortedJarOrPathList — a List that sorts its elements (record JarOrPath) by version. We replace the regular list for each layer with our list at the right time (configureLayerToBeSortedByVersion).

Automatic Module Names

With the above hack, all we need to do is inject a newer version with the same module name and our hack will choose the newer one. What could go wrong?
Forge derives the automatic module name (if one isn’t specified explicitly) from the file name.
The problem: If a user:
  • Downloads KFF from a different website
  • Downloads it twice and the browser adds (1) to the name
Then it gets a different automatic module name → two modules with overlapping packages → ModLauncher explodes. Solution: SelfRenamingJarMetadata — In an elegant way, this jar metadata object will:
  • On-the-fly when asked for its name
  • Look through all other jar metadata on the same layer
  • Check if any have overlapping packages
  • Steal their name
Implemented so well it would even work if another mod decided to do the same thing. More compatible than ModLauncher itself!

Upgrading Kotlin

KotlinForForge sometimes:
  • Takes a while to update
  • Does backwards incompatible changes
  • No longer updates at all for non-LTS versions
Why we can’t use the ML8 approach: We can’t just throw the Kotlin stdlib into the classloader because that classloader isn’t a simple URLClassLoader anymore. What about JiJ? As of mid-2022, ModLauncher finally supports proper Jar-in-Jar (they call it JarJar). Surely we can just upgrade the Kotlin stdlib itself?
Plot twist: It doesn’t actually work properly for language mods. KotlinForForge doesn’t/can’t use it.The issue was reported (MinecraftForge#8878) shortly after JiJ’s release. It is 2023 now and three major Forge versions later, it’s still in the “yeah, we know. deal with it.” stage.
Solution: We automatically replace the entire KotlinForForge jar with an automatically generated one that:
  • Is based on the original KFF
  • Has its bundled Kotlin upgraded from our bundle
Ugly details:
  • KFF still gets updated—we don’t want to downgrade Kotlin stdlibs
  • No nice way to know the version in the loaded KFF jar (it’s been exploded, metadata lost)
  • We get the coroutines version from a random file it has
  • Get the stdlib version by loading the stdlib KotlinVersion class in a temporary classloader
  • Take a good guess at the serialization version
  • It all just about works out

Platform Comparison

Simplest

Fabric - Clean APIs, minimal hacks needed

Most Hacks

LaunchWrapper - Oldest, most workarounds for modern software

Most Complex

ModLauncher 9 - Java modules + HashMap iteration = chaos

Best Designed

Fabric - Actually built to be a mod loader

Build docs developers (and LLMs) love