Skip to main content

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:
VersionPackageModContainer Constructor
0.11net.fabricmc.loader(LoaderModMetadata, URL)
0.12.2net.fabricmc.loader.impl(LoaderModMetadata, URL)
0.12.3net.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.discoveryModCandidate 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.

ModMenu Integration

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.

Build docs developers (and LLMs) love