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
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.
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:
| Aspect | CoreMod | Tweaker |
|---|
| Load timing | Very early (pre-Minecraft) | Early (pre-Minecraft) |
| FML integration | Native support | Limited support |
| With TweakClass | Not loaded by FML | Preferred |
| Mixin support | Manual setup | Automatic via MixinTweaker |
| Access to Launch | Limited | Full 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.