Skip to main content

Overview

The LaunchWrapper implementation is used on legacy Minecraft/Forge versions (1.7.10-1.12.2). It leverages Forge’s tweaker system and provides a sophisticated relaunch mechanism to upgrade mods and libraries at runtime.

Core Components

ITweaker Implementation

Essential Loader provides multi-stage tweaker implementations:

Stage 0: EssentialSetupTweaker

Location: stage0/launchwrapper/src/main/java/gg/essential/loader/stage0/EssentialSetupTweaker.java
public class EssentialSetupTweaker implements ITweaker {
    private final EssentialLoader loader = new EssentialLoader("launchwrapper");
    private final ITweaker stage1;

    public EssentialSetupTweaker() throws Exception {
        this.stage1 = loadStage1(this);
    }

    @Override
    public void injectIntoClassLoader(LaunchClassLoader classLoader) {
        this.stage1.injectIntoClassLoader(classLoader);
    }
}
Key responsibilities:
  • Extracts and loads stage1 from embedded JAR
  • Adds stage1 to LaunchClassLoader with exclusions
  • Delegates to stage1 tweaker
  • Hacks parent classloader using reflection to add stage1 URL

Stage 1: EssentialSetupTweaker

Location: stage1/launchwrapper/src/main/java/gg/essential/loader/stage1/EssentialSetupTweaker.java
public class EssentialSetupTweaker implements ITweaker {
    private final ITweaker stage2;

    public EssentialSetupTweaker(ITweaker stage0) throws Exception {
        if (DelayedStage0Tweaker.isRequired()) {
            DelayedStage0Tweaker.prepare(stage0);
            this.stage2 = null;
            return;
        }
        this.stage2 = newStage2Tweaker(stage0);
    }
}
Key features:
  • Extracts stage2 from embedded resources to temp file
  • Loads stage2 in child URLClassLoader with Launch.classLoader as parent
  • Handles delayed stage0 injection for compatibility
  • Caches stage2 class across instances

Stage 2: EssentialSetupTweaker

Location: stage2/launchwrapper/src/main/java/gg/essential/loader/stage2/EssentialSetupTweaker.java
public class EssentialSetupTweaker implements ITweaker {
    public static final RelaunchedLoader LOADER;
    
    static {
        RelaunchInfo relaunchInfo = RelaunchInfo.get();
        if (relaunchInfo == null) {
            Loader loader = new Loader();
            loader.loadAndRelaunch();
            throw new AssertionError("relaunch should not return");
        } else {
            LOADER = new RelaunchedLoader(relaunchInfo);
        }
    }

    public EssentialSetupTweaker(ITweaker stage0) {
        LOADER.initialize(stage0);
    }
}
Key features:
  • Detects if running pre-relaunch or post-relaunch via RelaunchInfo system property
  • Either initiates relaunch or sets up post-relaunch loader
  • Static initialization ensures single execution per classloader

Relaunch Mechanism

RelaunchInfo Data Structure

Location: stage2/launchwrapper/src/main/java/gg/essential/loader/stage2/RelaunchInfo.java:8
class RelaunchInfo {
    public Set<String> loadedIds;      // Mod IDs that were loaded
    public List<String> extraMods;     // Additional mods to inject

    public static RelaunchInfo get() {
        String json = System.getProperty("gg.essential.loader.stage2.relaunch-info");
        if (json == null) return null;
        return new Gson().fromJson(json, RelaunchInfo.class);
    }

    public static void put(RelaunchInfo value) {
        System.setProperty("gg.essential.loader.stage2.relaunch-info", new Gson().toJson(value));
    }
}
RelaunchInfo uses system properties to pass data across relaunches. This is the only mechanism that survives process re-initialization.

Relaunch Process

Location: stage2/launchwrapper/src/main/java/gg/essential/loader/stage2/relaunch/Relaunch.java:34
public static void relaunch(Set<URL> prioritizedUrls) {
    // Clean up global state
    cleanupForRelaunch();

    // Get system classloader's classpath
    URLClassLoader systemClassLoader = (URLClassLoader) Launch.class.getClassLoader();
    List<URL> urls = new ArrayList<>(Arrays.asList(systemClassLoader.getURLs()));

    // Remove tweaker jars to avoid duplicates
    Set<String> tweakClasses = getTweakClasses();
    urls.removeIf(url -> isTweaker(url, tweakClasses));

    // Prioritize our updated jars
    urls.removeIf(prioritizedUrls::contains);
    urls.addAll(0, prioritizedUrls);

    // Create new classloader and re-invoke Launch.main
    RelaunchClassLoader relaunchClassLoader = new RelaunchClassLoader(urls.toArray(new URL[0]), systemClassLoader);
    Class<?> innerLaunch = Class.forName(main, false, relaunchClassLoader);
    Method innerMainMethod = innerLaunch.getDeclaredMethod("main", String[].class);
    innerMainMethod.invoke(null, (Object) args.toArray(new String[0]));
}
Relaunch cleanup includes:
  • Clearing ModPatcher properties (nallar.ModPatcher.alreadyLoaded)
  • Removing Mixin’s log appender to prevent INIT phase conflicts
With non-beta log4j2, Mixin’s appender will be rejected if not cleaned up, causing all INIT-phase mixins to be skipped.

Tweaker Detection

Location: stage2/launchwrapper/src/main/java/gg/essential/loader/stage2/relaunch/Relaunch.java:146
private static boolean isTweaker(URL url, Set<String> tweakClasses) {
    File file = new File(url.toURI());
    try (JarFile jar = new JarFile(file)) {
        Manifest manifest = jar.getManifest();
        if (manifest == null) return false;
        return tweakClasses.contains(manifest.getMainAttributes().getValue("TweakClass"));
    }
}
Tweaker classes are derived from CoreModManager.tweakSorting field rather than ignoredModFiles since tweakers commonly remove themselves from the latter.

ClassLoader Manipulation

Adding to Launch ClassLoader

Location: stage0/launchwrapper/src/main/java/gg/essential/loader/stage0/EssentialSetupTweaker.java:76
private static void addUrlHack(ClassLoader loader, URL url) {
    // Reflect into parent URLClassLoader
    final ClassLoader classLoader = Launch.classLoader.getClass().getClassLoader();
    final Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
    method.setAccessible(true);
    method.invoke(classLoader, url);
}
This breaks if the parent classloader is not a URLClassLoader, but Forge has the same limitation.

ClassLoader Exclusions

Location: stage0/launchwrapper/src/main/java/gg/essential/loader/stage0/EssentialSetupTweaker.java:46
LaunchClassLoader classLoader = Launch.classLoader;
classLoader.addURL(stage1Url);
classLoader.addClassLoaderExclusion(STAGE1_PKG);
Exclusions force classes to be loaded by the parent classloader, essential for proper stage isolation.

Post-Relaunch Initialization

RelaunchedLoader

Location: stage2/launchwrapper/src/main/java/gg/essential/loader/stage2/RelaunchedLoader.java:31
public class RelaunchedLoader {
    private final RelaunchInfo relaunchInfo;
    private final List<SourceFile> sourceFiles;

    RelaunchedLoader(RelaunchInfo relaunchInfo) {
        this.relaunchInfo = relaunchInfo;
        this.sourceFiles = SourceFile.readInfos(Launch.classLoader.getSources());

        // Auto-inject MixinTweaker if mixin or essential was loaded
        if (relaunchInfo.loadedIds.contains("mixin")) {
            MixinTweakerInjector.injectMixinTweaker(true);
        }
    }

    public void initialize(ITweaker stage0Tweaker) {
        String tweakerName = stage0Tweaker.getClass().getName();
        for (SourceFile sourceFile : sourceFiles) {
            if (tweakerName.equals(sourceFile.tweaker)) {
                setupSourceFile(sourceFile);
            }
        }
    }
}

Source File Registration

Location: stage2/launchwrapper/src/main/java/gg/essential/loader/stage2/RelaunchedLoader.java:120
private void setupSourceFile(SourceFile sourceFile) {
    // Remove from Forge's ignored mod files
    CoreModManager.ignoredModFiles.remove(sourceFile.file.getName());

    // Add to reparse list
    CoreModManager.getReparseableCoremods().add(sourceFile.file.getName());

    // Load CoreMod manually if present (FML won't load it due to TweakClass)
    if (coreMod != null && !sourceFile.mixin) {
        loadCoreMod(sourceFile.file, coreMod);
    }

    // Inject MixinTweaker and register container
    if (sourceFile.mixin) {
        MixinTweakerInjector.injectMixinTweaker(false);
        // Add container to Mixin platform manager
        addContainer(sourceFile.file.toURI());
    }
}
FML ignores mods with TweakClass and won’t load their CoreMods. Essential manually loads CoreMods to maintain compatibility.

Mixin Integration

MixinTweaker Injection

Location: stage2/launchwrapper/src/main/java/gg/essential/loader/stage2/util/MixinTweakerInjector.java:16
public static void injectMixinTweaker(boolean canWait) {
    List<String> tweakClasses = (List<String>) Launch.blackboard.get("TweakClasses");

    // Check if already queued
    if (tweakClasses.contains(MIXIN_TWEAKER)) {
        if (!canWait) {
            // Initialize immediately for container registration
            newMixinTweaker();
        }
        return;
    }

    // Check if already initialized
    if (Launch.blackboard.get("mixin.initialised") != null) {
        return;
    }

    // Manually instantiate and add to Tweaks list
    List<ITweaker> tweaks = (List<ITweaker>) Launch.blackboard.get("Tweaks");
    tweaks.add(newMixinTweaker());
}
Adding MixinTweaker to TweakClasses during injectIntoClassLoader is too late. Must add directly to Tweaks list.

Compatibility Workarounds

Thread-Unsafe Transformers List

Location: stage2/launchwrapper/src/main/java/gg/essential/loader/stage2/compat/ThreadUnsafeTransformersListWorkaround.java:21
public static void apply() {
    LaunchClassLoader classLoader = Launch.classLoader;
    Field field = LaunchClassLoader.class.getDeclaredField("transformers");
    field.setAccessible(true);
    List<IClassTransformer> value = (List<IClassTransformer>) field.get(classLoader);
    field.set(classLoader, new CopyOnWriteArrayList<>(value));
}
Why needed: Forge registers transformers during mod loading when multiple threads are active, causing ConcurrentModificationException.

Mixin Transformer Exclusions

Location: stage2/launchwrapper/src/main/java/gg/essential/loader/stage2/RelaunchedLoader.java:90 Third-party transformers that fail when called multiple times:
addMixinTransformerExclusion("bre.smoothfont.asm.Transformer");
addMixinTransformerExclusion("com.therandomlabs.randompatches.core.RPTransformer");
addMixinTransformerExclusion("vazkii.quark.base.asm.ClassTransformer");

CoreMod vs Tweaker Approach

Differences:
AspectCoreModTweaker
Load timingVery early (pre-Minecraft)Early (pre-Minecraft)
FML integrationNative supportLimited support
With TweakClassNot loaded by FMLPreferred
Mixin supportManual setupAutomatic via MixinTweaker
Access to LaunchLimitedFull access
Essential’s approach: Use Tweaker as primary, manually load CoreMods if declared alongside TweakClass.

ASM Version Handling

LaunchWrapper uses ASM 5.0.3 by default. If you need newer ASM features, you must shade and relocate ASM in your jar.

Common Pitfalls

1. Stage Isolation

Problem: Classes loaded in wrong stage can cause ClassCastException. Solution: Use proper classloader exclusions:
classLoader.addClassLoaderExclusion("your.stage.package");

2. Relaunch Assumptions

Problem: Static fields are reset during relaunch. Solution: Use RelaunchInfo system property to pass data across relaunches.

3. Tweaker Ordering

Problem: Load order matters for compatibility. Solution: Check CoreModManager.tweakSorting for priority information.

4. Mixin Timing

Problem: Mixin containers must be registered before target classes load. Solution: Inject MixinTweaker with canWait=false and register containers immediately.

Build docs developers (and LLMs) love