Overview
The ModLauncher implementation supports modern Forge (1.13+) and NeoForge platforms. It provides version-specific compatibility layers for ModLauncher 8, 9, 10, and 11, with sophisticated module system integration and Kotlin stdlib upgrade mechanisms.
ModLauncher Version Support
| ModLauncher | Minecraft | Forge Version | Features |
|---|
| ML8 | 1.13-1.16 | Forge 28-36 | SortedLanguageLoadingProvider |
| ML9 | 1.17-1.20 | Forge 37-49 | Module system, SelfRenamingJarMetadata |
| ML10 | 1.20.2+ | Forge 49+, NeoForge 1.x | moduleDataProvider() API change |
| ML11 | 1.21+ | NeoForge 4+ | Merged FancyModLoader |
Core Components
Location: stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java:25
public class EssentialTransformationService implements ITransformationService {
private static CompatibilityLayer compatibilityLayer;
private final Path gameDir;
private final List<SecureJar> pluginJars = new ArrayList<>();
private final List<SecureJar> gameJars = new ArrayList<>();
private KFFMerger kffMerger;
private EssentialModLocator modLocator;
@Override
public String name() {
return "essential-loader";
}
@Override
public void initialize(IEnvironment environment) {
String mlVersion = environment.getProperty(IEnvironment.Keys.MLIMPL_VERSION.get()).orElseThrow();
compatibilityLayer = findCompatibilityLayerImpl(mlVersion);
modLocator = findModLocatorImpl();
kffMerger = new KFFMerger(compatibilityLayer);
}
@Override
public List<Resource> beginScanning(IEnvironment environment) {
// Load Essential and create PLUGIN layer resources
String gameVersion = determineGameVersion();
ActualEssentialLoader essentialLoader = new ActualEssentialLoader(gameDir, gameVersion, this);
essentialLoader.load();
injectMods();
configureLayerToBeSortedByVersion(IModuleLayerManager.Layer.PLUGIN);
return List.of(new Resource(IModuleLayerManager.Layer.PLUGIN, this.pluginJars));
}
@Override
public List<Resource> completeScan(IModuleLayerManager layerManager) {
if (!modsInjected) {
// Fallback: add to GAME layer for Mixin-only operation
configureLayerToBeSortedByVersion(IModuleLayerManager.Layer.GAME);
return Collections.singletonList(new Resource(IModuleLayerManager.Layer.GAME, this.gameJars));
}
return List.of();
}
}
Key lifecycle methods:
initialize() - Set up compatibility layer, detect ML version
beginScanning() - Load mods, inject into PLUGIN layer
completeScan() - Fallback injection, final layer setup
Compatibility Layer Pattern
Interface Definition
Location: stage2/modlauncher9/compatibility/src/main/java/gg/essential/loader/stage2/modlauncher/CompatibilityLayer.java:15
public interface CompatibilityLayer {
default SecureJar newSecureJarWithCustomMetadata(
BiFunction<Lazy<SecureJar>, JarMetadata, JarMetadata> metadataWrapper,
Path path
) {
return SecureJar.from(jar -> metadataWrapper.apply(new Lazy<>(jar), JarMetadata.from(jar, path)), path);
}
Manifest getManifest(SecureJar jar);
default Set<String> getPackages(SecureJar secureJar) {
return secureJar.getPackages();
}
default JarMetadata getJarMetadata(SecureJar secureJar, Path path) {
return JarMetadata.from(secureJar, path);
}
}
Version-Specific Implementations
ModLauncher 9 (Forge 37-49)
Location: stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/modlauncher/ML9CompatibilityLayer.java:7
public class ML9CompatibilityLayer implements CompatibilityLayer {
@Override
public Manifest getManifest(SecureJar jar) {
return jar.getManifest();
}
}
ModLauncher 10 (NeoForge 1.x)
Location: stage2/modlauncher9/modlauncher10/src/main/java/gg/essential/loader/stage2/modlauncher/ML10CompatibilityLayer.java:7
public class ML10CompatibilityLayer implements CompatibilityLayer {
@Override
public Manifest getManifest(SecureJar jar) {
return jar.moduleDataProvider().getManifest();
}
}
ModLauncher 10 moved manifest access to moduleDataProvider(). Using ML9’s API causes NoSuchMethodError.
ModLauncher 11 (NeoForge 4+)
Location: stage2/modlauncher9/modlauncher11/src/main/java/gg/essential/loader/stage2/modlauncher/ML11CompatibilityLayer.java
Handles FancyModLoader (FML) merger where ModLauncher version comes from fml_loader module instead of cpw.mods.modlauncher.
Automatic Version Detection
Location: stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java:88
private CompatibilityLayer findCompatibilityLayerImpl(String mlVersion) {
Map<String, String> impls;
if ("fml_loader".equals(ITransformationService.class.getModule().getName())) {
// NeoForge 1.21+ merged ModLauncher into FancyModLoader
impls = FMLML_COMPATIBILITY_IMPLEMENTATIONS;
} else {
impls = ML_COMPATIBILITY_IMPLEMENTATIONS;
}
for (Map.Entry<String, String> entry : impls.entrySet()) {
if (mlVersion.startsWith(entry.getKey())) {
String className = "gg.essential.loader.stage2.modlauncher." + entry.getValue();
return (CompatibilityLayer) Class.forName(className).getConstructor().newInstance();
}
}
throw new UnsupportedOperationException(
"Unable to find a matching compatibility layer for ModLauncher version " + mlVersion
);
}
Problem Statement
ModLauncher derives module names from jar files, making them unstable. Two jars with the same packages but different filenames create conflicting modules instead of upgrading.
Solution: Self-Renaming
Location: stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/SelfRenamingJarMetadata.java:32
public class SelfRenamingJarMetadata extends DelegatingJarMetadata {
private static final ThreadLocal<Boolean> RE_ENTRANCE_LOCK = ThreadLocal.withInitial(() -> false);
private final CompatibilityLayer compatibilityLayer;
private final Lazy<SecureJar> secureJar;
@Override
public String name() {
if (RE_ENTRANCE_LOCK.get()) {
throw new SelfRenamingReEntranceException();
}
RE_ENTRANCE_LOCK.set(true);
String defaultName = delegate.name();
Set<String> ourPackages = compatibilityLayer.getPackages(secureJar.get());
try {
for (SecureJar otherJar : getLayerJars()) {
String otherModuleName;
try {
otherModuleName = otherJar.name();
} catch (SelfRenamingReEntranceException ignored) {
continue;
}
Set<String> otherPackages = compatibilityLayer.getPackages(otherJar);
if (otherPackages.stream().anyMatch(ourPackages::contains)) {
LOGGER.debug("Found existing module with name {}, renaming {} to match.",
otherModuleName, defaultName);
return otherModuleName;
}
}
} finally {
RE_ENTRANCE_LOCK.set(false);
}
return defaultName;
}
}
Algorithm:
- Get our jar’s package list
- Iterate through all jars in current layer
- Find jar with overlapping packages
- Adopt its module name
- ModLauncher sees duplicate module name and picks newer version
Re-entrance detection prevents infinite loops when multiple jars try to rename to each other. Uses ThreadLocal to track call stack.
KotlinForForge Special Case
Location: stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/SelfRenamingJarMetadata.java:70
// Special case for fully-JarJar-reliant KFF which no longer contains code
if (defaultName.equals("thedarkcolour.kotlinforforge") &&
otherPackages.isEmpty() &&
isJarJarKff(otherJar)) {
LOGGER.debug("Found existing JarJar KFF with name {}, renaming {} to match.",
otherModuleName, defaultName);
return otherModuleName;
}
Newer KFF versions have no code and only JiJ Kotlin stdlib. They must still use the same module name to prevent duplicate Kotlin exports.
SortedJarOrPathList (ML9+)
Automatic Version Sorting
Location: stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java:168
private void configureLayerToBeSortedByVersion(IModuleLayerManager.Layer layer) {
try {
IModuleLayerManager layerManager = Launcher.INSTANCE.findLayerManager().orElseThrow();
Field layersField = layerManager.getClass().getDeclaredField("layers");
layersField.setAccessible(true);
Map<IModuleLayerManager.Layer, List<Object>> layers =
(Map<IModuleLayerManager.Layer, List<Object>>) layersField.get(layerManager);
layers.compute(layer, (__, list) -> {
SortedJarOrPathList sortedList = new SortedJarOrPathList(
compatibilityLayer,
kffMerger::maybeMergeInto
);
if (list != null) {
sortedList.addAll(list);
}
return sortedList;
});
} catch (Throwable t) {
LOGGER.error("Failed to replace mod list with sorted list:", t);
}
}
Why needed: ModLauncher’s JarModuleFinder picks the first jar registered for each module name. Registration order is random (HashMap iteration), so it effectively picks a random version.
Solution: Replace the layer’s jar list with a self-sorting list that orders by version.
SortedLanguageLoadingProvider (ML8)
Problem: Kotlin Stdlib Version Conflicts
Multiple mods may ship different Kotlin versions. ModLauncher needs to load only the newest version into the language classloader.
Solution: Version-Aware Language Provider
Location: stage2/modlauncher8/src/main/java/net/minecraftforge/fml/loading/SortedLanguageLoadingProvider.java:25
public class SortedLanguageLoadingProvider extends LanguageLoadingProvider {
private static final ArtifactVersion FALLBACK_VERSION = new DefaultArtifactVersion("1");
private static final Function<ModFile, Manifest> manifestGetter =
UnsafeHacks.makeGetter(ModFile.class, "manifest");
private static final Map<ModFile, ArtifactVersion> versionCache = new WeakHashMap<>();
// IMPORTANT: No constructors or field initializers!
// Instantiated via Unsafe.allocateInstance
public List<Path> extraHighPriorityFiles;
@Override
public void addAdditionalLanguages(List<ModFile> modFiles) {
if (modFiles == null) return;
Set<String> visited = new HashSet<>();
Stream<ModFile> filteredFiles = modFiles.stream()
.sorted(Comparator.comparing(this::getVersion).reversed())
.filter(modFile -> visited.add(getName(modFile)));
Stream<ModFile> extraFiles = extraHighPriorityFiles.stream()
.map(path -> new ModFile(path, null, null));
modFiles = Stream.concat(extraFiles, filteredFiles)
.collect(Collectors.toList());
super.addAdditionalLanguages(modFiles);
}
private static String getName(ModFile modFile) {
Attributes attributes = getLangProviderAttributes(modFile);
String title = attributes.getValue(Attributes.Name.IMPLEMENTATION_TITLE);
return title != null ? title : modFile.getFileName();
}
private ArtifactVersion getVersion(ModFile modFile) {
return versionCache.computeIfAbsent(modFile, __ -> {
Attributes attributes = getLangProviderAttributes(modFile);
String versionStr = attributes.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
return versionStr != null ? new DefaultArtifactVersion(versionStr) : FALLBACK_VERSION;
});
}
}
Unsafe Instantiation Hack
Location: stage2/modlauncher8/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java:115
UnsafeHacks.<FMLLoader, LanguageLoadingProvider>makeAccessor(FMLLoader.class, "languageLoadingProvider")
.update(null, oldProvider -> {
SortedLanguageLoadingProvider newProvider =
UnsafeHacks.allocateCopy(oldProvider, SortedLanguageLoadingProvider.class);
newProvider.extraHighPriorityFiles = getJars(true).collect(Collectors.toList());
return newProvider;
});
SortedLanguageLoadingProvider must not have constructors or field initializers because it’s instantiated via Unsafe.allocateInstance(). The super constructor is package-private and cannot be called.
Why allocateCopy: Copies fields from the original LanguageLoadingProvider instance to preserve its state.
ModLocator Registration
Dynamic ModLocator Selection
Location: stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java:113
private static EssentialModLocator findModLocatorImpl() {
String loader;
String version;
if (hasClass("net.neoforged.fml.loading.FMLLoader")) {
loader = "NeoForge";
if (!hasClass("net.neoforged.fml.loading.moddiscovery.AbstractJarFileModProvider")) {
version = "4_0_0"; // NeoForge 1.21+
} else {
version = "1_0_0"; // NeoForge 1.20.2-1.20.6
}
} else {
loader = "Forge";
if (hasClass("net.minecraftforge.forgespi.locating.IModLocator$ModFileOrException")) {
if (!hasClass("net.minecraftforge.fml.loading.moddiscovery.AbstractJarFileModLocator")) {
version = "49_0_38"; // Forge 1.20.2+
} else {
version = "41_0_34"; // Forge 1.19.3-1.20.1
}
} else {
if (hasClass("net.minecraftforge.fml.loading.moddiscovery.AbstractJarFileModLocator")) {
version = "40_1_60"; // Forge 1.18.2-1.19.2
} else {
version = "37_0_0"; // Forge 1.17.1-1.18.1
}
}
}
String clsName = "gg.essential.loader.stage2.modlauncher." + loader + "_" + version + "_ModLocator";
return (EssentialModLocator) Class.forName(clsName).getDeclaredConstructor().newInstance();
}
Available implementations:
Forge_37_0_0_ModLocator - Forge 1.17.1-1.18.1
Forge_40_1_60_ModLocator - Forge 1.18.2-1.19.2
Forge_41_0_34_ModLocator - Forge 1.19.3-1.20.1
Forge_49_0_38_ModLocator - Forge 1.20.2+
NeoForge_1_0_0_ModLocator - NeoForge 1.20.2-1.20.6
NeoForge_4_0_0_ModLocator - NeoForge 1.21+
ModLocator Interface
Location: stage2/modlauncher9/compatibility/src/main/java/gg/essential/loader/stage2/modlauncher/EssentialModLocator.java
public interface EssentialModLocator {
/**
* Inject mods into Forge's mod candidate list.
*
* @return true if injection succeeded, false for fallback to GAME layer
*/
boolean injectMods(List<SecureJar> gameJars);
}
Injection strategy: Reflect into ModValidator to add mods between stage 1 validation (creation) and stage 2 validation (parallel scanning).
Module System Interactions (ML9+)
Location: stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/DescriptorRewritingJarMetadata.java:21
public class DescriptorRewritingJarMetadata implements JarMetadata {
private final JarMetadata delegate;
private final JarMetadata newPkgsMeta;
@Override
public ModuleDescriptor descriptor() {
if (this.descriptor == null) {
ModuleDescriptor org = delegate.descriptor();
ModuleDescriptor.Builder builder = ModuleDescriptor.newModule(org.name(), org.modifiers());
// Update packages from new metadata
builder.packages(newPkgsMeta.descriptor().packages());
if (!org.isAutomatic()) {
org.requires().forEach(builder::requires);
org.exports().forEach(builder::exports);
if (!org.isOpen()) {
org.opens().forEach(builder::opens);
}
org.uses().forEach(builder::uses);
}
// Merge provided services
Map<String, List<String>> orgProvides = org.provides()
.stream().collect(Collectors.toMap(ModuleDescriptor.Provides::service, ModuleDescriptor.Provides::providers));
Map<String, List<String>> newProvides = newPkgsMeta.descriptor().provides()
.stream().collect(Collectors.toMap(ModuleDescriptor.Provides::service, ModuleDescriptor.Provides::providers));
Map<String, List<String>> mergedProvides = new HashMap<>(orgProvides);
mergedProvides.putAll(newProvides);
mergedProvides.forEach(builder::provides);
org.mainClass().ifPresent(builder::mainClass);
this.descriptor = builder.build();
}
return this.descriptor;
}
}
Why needed: When adding classes to a SecureJar in new packages, the module system needs the updated package list. ModLauncher builds a lookup table from this.
Failing to update the module descriptor when adding classes in new packages causes NoClassDefFoundError despite the class being physically present in the jar.
Kotlin Stdlib Upgrade Mechanism
KFFMerger
Location: stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java:55
private static final Pattern JIJ_KOTLIN_FILES =
Pattern.compile("kotlinx?-([a-z0-9-]+)-(\\d+\\.\\d+\\.\\d+)\\.jar");
public void addToClasspath(final Path path) {
final SecureJar jar = compatibilityLayer.newSecureJarWithCustomMetadata(
(j, metadata) -> new SelfRenamingJarMetadata(compatibilityLayer, j, metadata),
path
);
if (this.kffMerger.addKotlinJar(path, jar)) {
return; // Merged into KFF, don't add separately
}
if (determineLayer(jar) == IModuleLayerManager.Layer.PLUGIN) {
this.pluginJars.add(jar);
} else {
this.gameJars.add(jar);
}
}
Pattern matching: JiJ Kotlin files matching kotlinx?-([a-z0-9-]+)-(\d+\.\d+\.\d+)\.jar are merged into the language classloader instead of being injected as regular mods.
Kotlin must be in the PLUGIN layer (language classloader). Loading it in the GAME layer causes ClassCastException when KFF code interacts with user code.
Layer Management
Layer Determination
Location: stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java:65
private static IModuleLayerManager.Layer determineLayer(SecureJar jar) {
final String modType = compatibilityLayer.getManifest(jar)
.getMainAttributes()
.getValue("FMLModType");
if ("LANGPROVIDER".equals(modType) || "LIBRARY".equals(modType)) {
return IModuleLayerManager.Layer.PLUGIN;
} else {
return IModuleLayerManager.Layer.GAME;
}
}
Layers:
- PLUGIN - Language providers, libraries, core mods (parent of GAME)
- GAME - Regular mods and game classes
ML8-Specific Implementation
Location: stage2/modlauncher8/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java:34
public class EssentialTransformationService implements ITransformationService {
private final List<Path> jars = new ArrayList<>();
@Override
public void initialize(IEnvironment environment) {
// Register stage2 jar as containing mod locator
ModDirTransformerDiscoverer.getExtraLocators().add(getStage2JarPath());
// Pass jars to mod locator via environment property
environment.computePropertyIfAbsent(JARS_KEY.get(), __ -> jars);
// Register launch plugin for TransformingClassLoader setup
registerLaunchPlugin(new EssentialLaunchPluginService());
}
public static class ModLocator extends AbstractJarFileLocator {
public ModLocator() {
// Hijack LanguageLoadingProvider between initialization and first use
UnsafeHacks.<FMLLoader, LanguageLoadingProvider>makeAccessor(
FMLLoader.class, "languageLoadingProvider"
).update(null, oldProvider -> {
SortedLanguageLoadingProvider newProvider =
UnsafeHacks.allocateCopy(oldProvider, SortedLanguageLoadingProvider.class);
newProvider.extraHighPriorityFiles = getJars(true).collect(Collectors.toList());
return newProvider;
});
}
@Override
public List<IModFile> scanMods() {
return getJars(false)
.map(path -> ModFile.newFMLInstance(path, this))
.peek(modFile -> modJars.computeIfAbsent(modFile, this::createFileSystem))
.collect(Collectors.toList());
}
}
}
Key differences from ML9:
- No module system (Java 8)
- Uses
ModDirTransformerDiscoverer.getExtraLocators() for registration
- Mod locator loaded in dedicated classloader
- Communication via environment properties
Common Pitfalls
1. Module Name Conflicts
Problem: Two jars with same packages but different names create conflicts.
Solution: Use SelfRenamingJarMetadata to adopt existing module names.
2. Package List Outdated
Problem: Adding classes in new packages without updating descriptor.
Solution: Use DescriptorRewritingJarMetadata to merge package lists.
3. Random Version Selection
Problem: ModLauncher picks random version when multiple exist.
Solution: Use SortedJarOrPathList to enforce version ordering.
4. Kotlin in Wrong Layer
Problem: Kotlin loaded in GAME layer causes ClassCastException.
Solution: Check FMLModType manifest attribute, merge with KFF in PLUGIN layer.
5. Compatibility Layer Mismatch
Problem: Using wrong API for ModLauncher version.
Solution: Detect version via IEnvironment.Keys.MLIMPL_VERSION and load appropriate compatibility layer.
6. Manifest Access Changed (ML10)
Problem: SecureJar.getManifest() removed in ML10.
Solution: Use jar.moduleDataProvider().getManifest() for ML10+.
7. ModLocator API Differences
Problem: Forge/NeoForge have different ModLocator APIs across versions.
Solution: Detect via class presence and instantiate version-specific implementation.