Overview
The Fabric implementation uses Fabric Loader’s PreLaunch entrypoint system and provides sophisticated fake mod registration and JiJ (Jar-in-Jar) dependency handling.
Core Components
PreLaunch Entrypoint
Stage 1: EssentialSetupPreLaunch
Location: stage1/fabric/src/main/java/gg/essential/loader/stage1/EssentialSetupPreLaunch.java
public class EssentialSetupPreLaunch implements PreLaunchEntrypoint {
private final EssentialLoader loader;
public EssentialSetupPreLaunch(final PreLaunchEntrypoint stage0) throws Exception {
final FabricLoader fabricLoader = FabricLoader.getInstance();
final String mcVersion = fabricLoader.getModContainer("minecraft")
.map(it -> it.getMetadata().getVersion().getFriendlyString())
.orElse("unknown");
this.loader = EssentialLoader.getInstance("fabric_" + mcVersion);
this.loader.load(fabricLoader.getGameDir());
}
@Override
public void onPreLaunch() {
this.loader.initialize();
}
}
Key features:
- Implements
PreLaunchEntrypoint interface
- Runs before game initialization but after Fabric Loader setup
- Accepts stage0 entrypoint for delegation pattern
- Determines platform version from Minecraft mod metadata
Stage 2: EssentialLoader
Location: stage2/fabric/src/main/java/gg/essential/loader/stage2/EssentialLoader.java:38
public class EssentialLoader extends EssentialLoaderBase {
private final LoaderInternals loaderInternals = new LoaderInternals();
public EssentialLoader(Path gameDir, String gameVersion) {
super(gameDir, gameVersion);
// Optional debug logging
String debugLog = System.getProperty("essential.debuglog");
if (debugLog != null) {
Log4j2Hacks.addDebugLogFile(Level.getLevel(debugLog.toUpperCase(Locale.ROOT)));
}
}
@Override
protected ClassLoader getModClassLoader() {
return this.getClass()
.getClassLoader() // stage2 loader
.getParent() // stage1 loader
.getParent(); // fabric mod loader
}
}
Fake Mod Registration
Adding to Fabric Loader Internals
Location: stage2/fabric/src/main/java/gg/essential/loader/stage2/EssentialLoader.java:371
private void injectFakeMod(final Path path, final URL url, final ModMetadata metadata) {
FabricLoader fabricLoader = FabricLoader.getInstance();
Class<?> fabricLoaderClass = findFabricLoaderClass(fabricLoader);
// Access internal fields via reflection
Field modMapField = fabricLoaderClass.getDeclaredField("modMap");
Field modsField = fabricLoaderClass.getDeclaredField("mods");
Field entrypointStorageField = fabricLoaderClass.getDeclaredField("entrypointStorage");
Field adapterMapField = fabricLoaderClass.getDeclaredField("adapterMap");
modMapField.setAccessible(true);
modsField.setAccessible(true);
entrypointStorageField.setAccessible(true);
adapterMapField.setAccessible(true);
List<Object> mods = (List<Object>) modsField.get(fabricLoader);
Map<String, Object> modMap = (Map<String, Object>) modMapField.get(fabricLoader);
Object entrypointStorage = entrypointStorageField.get(fabricLoader);
Map<String, LanguageAdapter> adapterMap = (Map<String, LanguageAdapter>) adapterMapField.get(fabricLoader);
// Load mixin configs
Method getMixinConfigs = LoaderModMetadata.getDeclaredMethod("getMixinConfigs", EnvType.class);
for (String mixinConfig : (Collection<String>) getMixinConfigs.invoke(metadata, EnvType.CLIENT)) {
Mixins.addConfiguration(mixinConfig);
}
// Create and register mod container
Object modContainer = createModContainer(path, url, metadata);
mods.add(modContainer);
modMap.put(metadata.getId(), modContainer);
// Register entrypoints
registerEntrypoints(modContainer, metadata, entrypointStorage, adapterMap);
}
Fabric Loader’s internal class structure changes between versions. The code handles fabric-loader 0.11, 0.12, 0.13, 0.14+ with version-specific reflection paths.
Fabric Loader Version Compatibility
Location: stage2/fabric/src/main/java/gg/essential/loader/stage2/EssentialLoader.java:199
private Class<?> findImplClass(final String name) throws ClassNotFoundException {
try {
// fabric loader 0.12+
return Class.forName("net.fabricmc.loader.impl." + name);
} catch (ClassNotFoundException e) {
// fabric loader 0.11
return Class.forName("net.fabricmc.loader." + name);
}
}
Version-specific handling:
| Version | Package | ModContainer Constructor |
|---|
| 0.11 | net.fabricmc.loader | (LoaderModMetadata, URL) |
| 0.12.2 | net.fabricmc.loader.impl | (LoaderModMetadata, URL) |
| 0.12.3 | net.fabricmc.loader.impl | (LoaderModMetadata, Path) |
| 0.13+ | net.fabricmc.loader.impl | (ModCandidate) |
| 0.14+ | net.fabricmc.loader.impl | (ModCandidate) with extra params |
| 0.14.11+ | net.fabricmc.loader.impl | (ModCandidate) with isDev param |
| 0.16.0+ | net.fabricmc.loader.impl.discovery | ModCandidate vs ModCandidateImpl |
Jar-in-Jar (JiJ) Dependencies
JarInJarDependenciesHandler
Location: stage2/fabric/src/main/java/gg/essential/loader/stage2/jij/JarInJarDependenciesHandler.java:36
public class JarInJarDependenciesHandler {
private static final String SYNTHETIC_MOD_ID = "essential-dependencies";
private static final String SYNTHETIC_MOD_NAME = "Essential Dependencies";
private final Map<String, Path> updates = new HashMap<>();
private final List<String> updatedModNames = new ArrayList<>();
private final List<Path> modsToDisable = new ArrayList<>();
private final Path extractedJarsRoot;
public List<Path> loadMod(Path path) {
// Check if mod is already loaded
ModContainer loadedMod = FabricLoader.getInstance()
.getModContainer(modId).orElse(null);
if (loadedMod == null) {
// Not loaded, can inject directly
jarsToLoad.add(path);
return jarsToLoad;
}
// Check if loaded version is up-to-date
Version loadedVersion = loadedMod.getMetadata().getVersion();
if (loadedVersion >= modVersion) {
return jarsToLoad; // Already up-to-date
}
// Outdated, queue for update via synthetic mod
updates.put(modId, path);
updatedModNames.add(modName);
return Collections.emptyList();
}
}
Synthetic Mod Creation
Location: stage2/fabric/src/main/java/gg/essential/loader/stage2/jij/JarInJarDependenciesHandler.java:284
private void updateSyntheticMod() throws IOException {
Path modsFolder = fabricLoader.getGameDir().resolve("mods");
Path syntheticModPath = modsFolder.resolve("Essential Dependencies.jar");
// Create empty jar if doesn't exist
if (Files.notExists(syntheticModPath)) {
createEmptyJar(syntheticModPath);
}
try (SyntheticModJar syntheticModJar = new SyntheticModJar(syntheticModPath, SYNTHETIC_MOD_ID, SYNTHETIC_MOD_NAME)) {
// Clean up unused bundled mods
for (SyntheticModJar.InnerJar innerJar : syntheticModJar.getInnerJars()) {
if (updates.containsKey(innerJar.getId())) {
syntheticModJar.removeInnerJar(innerJar);
continue;
}
if (!isCurrentlyInUse(innerJar)) {
syntheticModJar.removeInnerJar(innerJar);
}
}
// Add all new updates
for (Map.Entry<String, Path> update : updates.entrySet()) {
syntheticModJar.addInnerJar(update.getValue());
}
}
}
Updating JiJ dependencies requires a restart because Fabric Loader resolves all mods before PreLaunch entrypoints run. The synthetic mod is created in the mods folder and loaded on next launch.
Old Fabric Loader Workaround
Location: stage2/fabric/src/main/java/gg/essential/loader/stage2/jij/JarInJarDependenciesHandler.java:46
private static final boolean NEED_TO_DISABLE_USER_MODS;
static {
boolean isOldLoader;
try {
Class.forName("net.fabricmc.loader.discovery.ModCandidateSet");
isOldLoader = true;
} catch (ClassNotFoundException e) {
isOldLoader = false;
}
NEED_TO_DISABLE_USER_MODS = isOldLoader;
}
Why needed: Fabric Loader prior to 0.12 doesn’t consider JiJ mods if the same mod is installed directly. Solution: rename user-installed versions to .jar.disabled.
Windows doesn’t allow renaming files that are currently in use. File renames are delayed until after game exit.
Development Environment
Runtime Mod Remapping
Location: stage2/fabric/src/main/java/gg/essential/loader/stage2/RuntimeModRemapper.java:51
public Path remap(final Path path, final ModMetadata metadata) throws Exception {
final Path devPath = Utils.mapFileBaseName(path, name -> name + "-dev");
final Path devInputs = devPath.resolveSibling(devPath.getFileName() + ".inputs");
final byte[] currentInputs = gatherRemapInputs(path);
// Check if cached remap is still valid
if (!Files.exists(devPath) ||
!Files.exists(devInputs) ||
!Arrays.equals(Files.readAllBytes(devInputs), currentInputs)) {
LOGGER.info("Remapping Essential to development mappings...");
loaderInternals.remapMod(metadata, path, devPath);
Files.write(devInputs, currentInputs);
}
return devPath;
}
Cache invalidation based on:
- Mappings file hash (
mappings/mappings.tiny)
- Remap classpath (
fabric.remapClasspathFile system property)
- Input jar hash
Accessing Fabric Internals for Remapping
Location: stage2/fabric/src/main/java/gg/essential/loader/stage2/EssentialLoader.java:264
public void remapMod(ModMetadata metadata, Path inputPath, Path outputPath) throws Exception {
Class<?> ModCandidate = findImplClass("discovery.ModCandidate");
Class<?> RuntimeModRemapper = findImplClass("discovery.RuntimeModRemapper");
Object candidate = createCandidate(inputPath, inputPath.toUri().toURL(), metadata);
try {
// fabric loader 0.11: in-memory filesystem
Method remap = RuntimeModRemapper.getDeclaredMethod("remap", Collection.class, FileSystem.class);
FileSystem fileSystem = ModResolver.getInMemoryFs();
Object result = remap.invoke(null, Collections.singleton(candidate), fileSystem);
} catch (NoSuchMethodException e) {
// fabric loader 0.12+: temp directories
Method remap = RuntimeModRemapper.getDeclaredMethod("remap", Collection.class, Path.class, Path.class);
Path tmpDir = Files.createTempDirectory("remap-tmp");
Path outDir = Files.createTempDirectory("remap-out");
remap.invoke(null, Collections.singleton(candidate), tmpDir, outDir);
}
}
ClassLoader Management
Adding to Mod ClassLoader
Location: stage2/fabric/src/main/java/gg/essential/loader/stage2/EssentialLoader.java:75
private void addToClassLoader(final URL url) {
try {
this.loaderInternals.addToClassLoaderViaFabricLauncherBase(url);
return;
} catch (Throwable t) {
LOGGER.warn("Failed to add URL to classpath via FabricLauncherBase:", t);
}
try {
this.loaderInternals.addToClassLoaderViaReflection(url);
return;
} catch (Throwable t) {
LOGGER.warn("Failed to add URL to classpath via classloader reflection:", t);
}
throw new RuntimeException("Failed to add Essential jar to parent ClassLoader.");
}
Method 1: FabricLauncherBase (preferred)
private void addToClassLoaderViaFabricLauncherBase(final URL url) {
FabricLauncherBase.getLauncher().propose(url);
}
Method 2: Reflection fallback
private void addToClassLoaderViaReflection(final URL url) {
final ClassLoader classLoader = getModClassLoader();
final Method method = classLoader.getClass().getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
method.invoke(classLoader, url);
}
Mixin Chain-Loading
Handling Premature Class Loading
Location: stage2/fabric/src/main/java/gg/essential/loader/stage2/EssentialLoader.java:178
private void chainLoadMixins() throws ReflectiveOperationException {
if (Mixins.getUnvisitedCount() == 0) {
return; // Mixin already chain-loaded our config
}
// Force Mixin to load our configs immediately
MixinEnvironment environment = MixinEnvironment.getDefaultEnvironment();
Object transformer = environment.getActiveTransformer();
Field processorField = transformer.getClass().getDeclaredField("processor");
processorField.setAccessible(true);
Object processor = processorField.get(transformer);
Method select = processor.getClass().getDeclaredMethod("select", MixinEnvironment.class);
select.setAccessible(true);
select.invoke(processor, environment);
}
If a third-party preLaunch entrypoint loads MC classes before Essential registers mixins, those mixins won’t be applied. This desperate measure manually triggers mixin config loading.
Example problematic scenario: Kambrik issue #7 - accidentally loading MC classes in preLaunch.
Location: stage2/fabric/src/main/java/gg/essential/loader/stage2/EssentialLoader.java:149
try {
addFakeMod(path, url);
} catch (Throwable t) {
LOGGER.warn("Failed to add dummy mod container. Essential will be missing from mod menu.", t);
}
Fake mod registration is optional - failure only affects ModMenu visibility, not functionality.
Kotlin Stdlib Special Case
Location: stage2/fabric/src/main/java/gg/essential/loader/stage2/jij/JarInJarDependenciesHandler.java:197
private boolean isAlreadyOnClasspath(String modId) {
String cls = null;
switch (modId) {
case "org_jetbrains_kotlin_kotlin-stdlib":
cls = "kotlin/Unit.class"; break;
case "org_jetbrains_kotlin_kotlin-stdlib-jdk7":
cls = "kotlin/jdk7/AutoCloseableKt.class"; break;
case "org_jetbrains_kotlin_kotlin-stdlib-jdk8":
cls = "kotlin/jvm/jdk8/JvmRepeatableKt.class"; break;
case "org_jetbrains_kotlin_kotlin-reflect":
cls = "kotlin/reflect/jvm/KTypesJvm.class"; break;
case "org_jetbrains_kotlinx_kotlinx-coroutines-core-jvm":
cls = "kotlinx/coroutines/Job.class"; break;
// ...
}
return cls != null && getClass().getClassLoader().getResource(cls) != null;
}
In dev environments, if Kotlin is on the app classpath (e.g., Gradle dependency), loading it again in the mod classloader causes two instances to coexist, which breaks when they interact.
Log4j2 Hacks
Location: stage2/fabric/src/main/java/gg/essential/loader/stage2/Log4j2Hacks.java
Provides addDebugLogFile() method for debug logging when essential.debuglog system property is set.
Common Pitfalls
1. Entrypoint Timing
Problem: PreLaunch runs after Fabric Loader resolves all mods.
Solution: Use synthetic mod for JiJ dependencies that require restart.
2. Fabric Loader Version Changes
Problem: Internal APIs change between versions.
Solution: Use multi-version reflection with fallbacks.
3. Mixin Config Registration
Problem: Mixin configs must be registered before target classes load.
Solution: Register during fake mod injection, with manual chain-loading as fallback.
4. ClassLoader Hierarchy
Problem: Wrong classloader can cause ClassNotFoundException.
Solution: Traverse hierarchy: this.getClass().getClassLoader().getParent().getParent()
5. Development vs Production Mappings
Problem: Mods use production mappings, dev environment uses intermediary.
Solution: Runtime remapping with cache based on mappings/classpath hash.