diff --git a/build.gradle b/build.gradle index 3aae2ec..44cd116 100644 --- a/build.gradle +++ b/build.gradle @@ -9,9 +9,14 @@ subprojects { def javaVersions = [ "compatibility": 16, "modlauncher9": 16, + "forge37": 16, "forge40": 17, + "forge41": 17, "forge49": 17, + "neoforge1": 17, + "neoforge4": 21, "modlauncher10": 17, + "modlauncher11": 21, ] java.toolchain.languageVersion.set(JavaLanguageVersion.of(javaVersions.getOrDefault(project.name, 8))) diff --git a/settings.gradle.kts b/settings.gradle.kts index 3de994f..3bc4712 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,9 +23,14 @@ include(":stage2:modlauncher") include(":stage2:modlauncher8") include(":stage2:modlauncher9") include(":stage2:modlauncher9:compatibility") +include(":stage2:modlauncher9:forge37") include(":stage2:modlauncher9:forge40") +include(":stage2:modlauncher9:forge41") include(":stage2:modlauncher9:forge49") +include(":stage2:modlauncher9:neoforge1") +include(":stage2:modlauncher9:neoforge4") include(":stage2:modlauncher9:modlauncher10") +include(":stage2:modlauncher9:modlauncher11") include(":integrationTest:common") include(":integrationTest:fabric") include(":integrationTest:launchwrapper") diff --git a/stage0/build.gradle b/stage0/build.gradle index 3aa634e..76a9f03 100644 --- a/stage0/build.gradle +++ b/stage0/build.gradle @@ -1,4 +1,4 @@ -version = "1.2.3" +version = "1.2.4" configure(subprojects.findAll { it.name != "common" && it.name != "modlauncher" }) { apply plugin: 'maven-publish' diff --git a/stage1/modlauncher9/build.gradle b/stage1/modlauncher9/build.gradle index 6cbb65f..c085c3e 100644 --- a/stage1/modlauncher9/build.gradle +++ b/stage1/modlauncher9/build.gradle @@ -9,5 +9,6 @@ dependencies { compileOnly("net.sf.jopt-simple:jopt-simple:5.0.4") compileOnly("org.jetbrains:annotations:21.0.1") - compileOnly("net.minecraftforge:fmlloader:1.17.1-37.0.82") + // provided by fmlloader (both forge and neoforge ones) + compileOnly("org.apache.logging.log4j:log4j-api:2.14.1") } diff --git a/stage1/modlauncher9/src/main/java/gg/essential/loader/stage1/EssentialTransformationService.java b/stage1/modlauncher9/src/main/java/gg/essential/loader/stage1/EssentialTransformationService.java index 9fa54d5..8bc60ad 100644 --- a/stage1/modlauncher9/src/main/java/gg/essential/loader/stage1/EssentialTransformationService.java +++ b/stage1/modlauncher9/src/main/java/gg/essential/loader/stage1/EssentialTransformationService.java @@ -4,7 +4,6 @@ import cpw.mods.modlauncher.api.NamedPath; import gg.essential.loader.stage1.util.FallbackTransformationService; import gg.essential.loader.stage1.util.IDelegatingTransformationService; -import net.minecraftforge.fml.loading.ModDirTransformerDiscoverer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -44,7 +43,18 @@ private void setupSourceFile(final Path sourceFile) throws Exception { // Forge will by default ignore a mod file if it contains an implementation of ITransformationService // So we need to remove ourselves from that exclusion list - Field foundField = ModDirTransformerDiscoverer.class.getDeclaredField("found"); + Class cls; + try { + cls = Class.forName("net.minecraftforge.fml.loading.ModDirTransformerDiscoverer"); + } catch (ClassNotFoundException e1) { + try { + cls = Class.forName("net.neoforged.fml.loading.ModDirTransformerDiscoverer"); + } catch (ClassNotFoundException e2) { + e2.addSuppressed(e1); + throw e2; + } + } + Field foundField = cls.getDeclaredField("found"); foundField.setAccessible(true); ((List) foundField.get(null)).removeIf(namedPath -> Arrays.stream(namedPath.paths()).anyMatch(path -> path.normalize().equals(normalizedSourceFile))); diff --git a/stage2/build.gradle b/stage2/build.gradle index b6dd180..11ee558 100644 --- a/stage2/build.gradle +++ b/stage2/build.gradle @@ -1,4 +1,4 @@ -version = "1.6.3" +version = "1.6.4" configure(subprojects) { version = parent.version diff --git a/stage2/common/src/main/java/gg/essential/loader/stage2/util/Lazy.java b/stage2/common/src/main/java/gg/essential/loader/stage2/util/Lazy.java new file mode 100644 index 0000000..44c848d --- /dev/null +++ b/stage2/common/src/main/java/gg/essential/loader/stage2/util/Lazy.java @@ -0,0 +1,26 @@ +package gg.essential.loader.stage2.util; + +import java.util.function.Supplier; + +public class Lazy { + private Supplier supplier; + private T value; + + public Lazy(Supplier supplier) { + this.supplier = supplier; + } + + public Lazy(T value) { + this.value = value; + } + + public T get() { + if (supplier != null) { + synchronized (this) { + value = supplier.get(); + supplier = null; + } + } + return value; + } +} diff --git a/stage2/modlauncher9/build.gradle b/stage2/modlauncher9/build.gradle index 3c50a53..7448cd5 100644 --- a/stage2/modlauncher9/build.gradle +++ b/stage2/modlauncher9/build.gradle @@ -4,9 +4,14 @@ repositories { dependencies { bundle(implementation(project("compatibility"))) + bundle(project("forge37")) bundle(project("forge40")) + bundle(project("forge41")) bundle(project("forge49")) + bundle(project("neoforge1")) + bundle(project("neoforge4")) bundle(project("modlauncher10")) + bundle(project("modlauncher11")) compileOnly("cpw.mods:modlauncher:9.0.7") // modlauncher uses these in its api but does not declare them as such @@ -14,6 +19,7 @@ dependencies { compileOnly("net.sf.jopt-simple:jopt-simple:5.0.4") compileOnly("org.jetbrains:annotations:21.0.1") + // provided by fmlloader (both forge and neoforge ones) compileOnly("org.apache.logging.log4j:log4j-api:2.8.1") - compileOnly("net.minecraftforge:fmlloader:1.17.1-37.0.82") + compileOnly("org.apache.maven:maven-artifact:3.8.1") } diff --git a/stage2/modlauncher9/compatibility/build.gradle b/stage2/modlauncher9/compatibility/build.gradle index faaf246..91a2ba4 100644 --- a/stage2/modlauncher9/compatibility/build.gradle +++ b/stage2/modlauncher9/compatibility/build.gradle @@ -3,7 +3,8 @@ repositories { } dependencies { + compileOnly(project(":stage2:common")) + compileOnly("cpw.mods:securejarhandler:0.9.50") compileOnly("cpw.mods:modlauncher:9.0.7") - compileOnly("net.minecraftforge:fmlloader:1.17.1-37.0.82") } \ No newline at end of file diff --git a/stage2/modlauncher9/compatibility/src/main/java/gg/essential/loader/stage2/modlauncher/CompatibilityLayer.java b/stage2/modlauncher9/compatibility/src/main/java/gg/essential/loader/stage2/modlauncher/CompatibilityLayer.java index a6e2a54..e832ede 100644 --- a/stage2/modlauncher9/compatibility/src/main/java/gg/essential/loader/stage2/modlauncher/CompatibilityLayer.java +++ b/stage2/modlauncher9/compatibility/src/main/java/gg/essential/loader/stage2/modlauncher/CompatibilityLayer.java @@ -1,14 +1,29 @@ package gg.essential.loader.stage2.modlauncher; +import cpw.mods.jarhandling.JarMetadata; import cpw.mods.jarhandling.SecureJar; +import gg.essential.loader.stage2.util.Lazy; +import java.nio.file.Path; +import java.util.Set; +import java.util.function.BiFunction; import java.util.jar.Manifest; /** * Provides abstractions for things which changed after ModLauncher 9. */ public interface CompatibilityLayer { + default SecureJar newSecureJarWithCustomMetadata(BiFunction, JarMetadata, JarMetadata> metadataWrapper, Path path) { + return SecureJar.from(jar -> metadataWrapper.apply(new Lazy<>(jar), JarMetadata.from(jar, path)), path); + } + Manifest getManifest(SecureJar jar); - EssentialModLocator makeModLocator(); + default Set getPackages(SecureJar secureJar) { + return secureJar.getPackages(); + } + + default JarMetadata getJarMetadata(SecureJar secureJar, Path path) { + return JarMetadata.from(secureJar, path); + } } diff --git a/stage2/modlauncher9/compatibility/src/main/java/gg/essential/loader/stage2/modlauncher/EssentialModLocator.java b/stage2/modlauncher9/compatibility/src/main/java/gg/essential/loader/stage2/modlauncher/EssentialModLocator.java index a3c7aac..bc7c2d6 100644 --- a/stage2/modlauncher9/compatibility/src/main/java/gg/essential/loader/stage2/modlauncher/EssentialModLocator.java +++ b/stage2/modlauncher9/compatibility/src/main/java/gg/essential/loader/stage2/modlauncher/EssentialModLocator.java @@ -1,13 +1,9 @@ package gg.essential.loader.stage2.modlauncher; -import net.minecraftforge.fml.loading.moddiscovery.ModFile; -import net.minecraftforge.forgespi.locating.IModFile; -import net.minecraftforge.forgespi.locating.IModLocator; +import cpw.mods.jarhandling.SecureJar; -import java.nio.file.Path; -import java.util.function.Consumer; -import java.util.stream.Stream; +import java.util.List; -public interface EssentialModLocator extends IModLocator { - Iterable scanMods(Stream paths); +public interface EssentialModLocator { + boolean injectMods(List modJars) throws ReflectiveOperationException; } diff --git a/stage2/modlauncher9/forge37/build.gradle b/stage2/modlauncher9/forge37/build.gradle new file mode 100644 index 0000000..ea01b97 --- /dev/null +++ b/stage2/modlauncher9/forge37/build.gradle @@ -0,0 +1,10 @@ +repositories { + maven { url "https://maven.minecraftforge.net/" } +} + +dependencies { + compileOnly(parent.project("compatibility")) + + compileOnly("net.minecraftforge:fmlloader:1.17.1-37.0.82") + compileOnly("cpw.mods:securejarhandler:0.9.50") +} diff --git a/stage2/modlauncher9/forge37/src/main/java/gg/essential/loader/stage2/modlauncher/EssentialModLocator_Forge.java b/stage2/modlauncher9/forge37/src/main/java/gg/essential/loader/stage2/modlauncher/EssentialModLocator_Forge.java new file mode 100644 index 0000000..7b5871b --- /dev/null +++ b/stage2/modlauncher9/forge37/src/main/java/gg/essential/loader/stage2/modlauncher/EssentialModLocator_Forge.java @@ -0,0 +1,39 @@ +package gg.essential.loader.stage2.modlauncher; + +import cpw.mods.jarhandling.SecureJar; +import net.minecraftforge.fml.loading.FMLLoader; +import net.minecraftforge.fml.loading.moddiscovery.ModFile; +import net.minecraftforge.fml.loading.moddiscovery.ModValidator; +import net.minecraftforge.forgespi.locating.IModLocator; + +import java.lang.reflect.Field; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; + +public interface EssentialModLocator_Forge extends EssentialModLocator, IModLocator { + Iterable scanMods(Stream paths); + + @Override + default boolean injectMods(List modJars) throws ReflectiveOperationException { + Field modValidatorField = FMLLoader.class.getDeclaredField("modValidator"); + modValidatorField.setAccessible(true); + ModValidator modValidator = (ModValidator) modValidatorField.get(null); + + if (modValidator == null) { + return false; + } + + Field candidateModsField = ModValidator.class.getDeclaredField("candidateMods"); + candidateModsField.setAccessible(true); + @SuppressWarnings("unchecked") + List modFiles = (List) candidateModsField.get(modValidator); + + for (ModFile modFile : this.scanMods(modJars.stream().map(SecureJar::getPrimaryPath))) { + modFile.identifyMods(); + modFiles.add(modFile); + } + + return true; + } +} diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_37_0_0_ModLocator.java b/stage2/modlauncher9/forge37/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_37_0_0_ModLocator.java similarity index 85% rename from stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_37_0_0_ModLocator.java rename to stage2/modlauncher9/forge37/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_37_0_0_ModLocator.java index 315343f..5b5adb3 100644 --- a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_37_0_0_ModLocator.java +++ b/stage2/modlauncher9/forge37/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_37_0_0_ModLocator.java @@ -7,9 +7,10 @@ import java.util.Map; import java.util.stream.Stream; -class Forge_37_0_0_ModLocator extends AbstractJarFileLocator implements EssentialModLocator { +public class Forge_37_0_0_ModLocator extends AbstractJarFileLocator implements EssentialModLocator_Forge { private Stream paths; + @Override public Iterable scanMods(Stream paths) { this.paths = paths; return scanMods().stream().map(it -> (ModFile) it)::iterator; diff --git a/stage2/modlauncher9/forge40/build.gradle b/stage2/modlauncher9/forge40/build.gradle index 2f367ac..98d8821 100644 --- a/stage2/modlauncher9/forge40/build.gradle +++ b/stage2/modlauncher9/forge40/build.gradle @@ -4,6 +4,7 @@ repositories { dependencies { compileOnly(parent.project("compatibility")) + compileOnly(parent.project("forge37")) compileOnly("cpw.mods:modlauncher:9.0.7") // modlauncher uses these in its api but does not declare them as such diff --git a/stage2/modlauncher9/forge40/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_40_1_60_ModLocator.java b/stage2/modlauncher9/forge40/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_40_1_60_ModLocator.java index 3979eef..56a7328 100644 --- a/stage2/modlauncher9/forge40/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_40_1_60_ModLocator.java +++ b/stage2/modlauncher9/forge40/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_40_1_60_ModLocator.java @@ -7,9 +7,10 @@ import java.util.Map; import java.util.stream.Stream; -class Forge_40_1_60_ModLocator extends AbstractJarFileModLocator implements EssentialModLocator { +public class Forge_40_1_60_ModLocator extends AbstractJarFileModLocator implements EssentialModLocator_Forge { private Stream paths; + @Override public Iterable scanMods(Stream paths) { this.paths = paths; return scanMods().stream().map(it -> (ModFile) it)::iterator; diff --git a/stage2/modlauncher9/forge41/build.gradle b/stage2/modlauncher9/forge41/build.gradle new file mode 100644 index 0000000..00b8f1f --- /dev/null +++ b/stage2/modlauncher9/forge41/build.gradle @@ -0,0 +1,10 @@ +repositories { + maven { url "https://maven.minecraftforge.net/" } +} + +dependencies { + compileOnly(parent.project("compatibility")) + compileOnly(parent.project("forge37")) + + compileOnly("net.minecraftforge:fmlloader:1.19-41.0.64") +} diff --git a/stage2/modlauncher9/modlauncher10/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_41_0_34_ModLocator.java b/stage2/modlauncher9/forge41/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_41_0_34_ModLocator.java similarity index 85% rename from stage2/modlauncher9/modlauncher10/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_41_0_34_ModLocator.java rename to stage2/modlauncher9/forge41/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_41_0_34_ModLocator.java index 48267d8..aaf630a 100644 --- a/stage2/modlauncher9/modlauncher10/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_41_0_34_ModLocator.java +++ b/stage2/modlauncher9/forge41/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_41_0_34_ModLocator.java @@ -7,9 +7,10 @@ import java.util.Map; import java.util.stream.Stream; -class Forge_41_0_34_ModLocator extends AbstractJarFileModLocator implements EssentialModLocator { +public class Forge_41_0_34_ModLocator extends AbstractJarFileModLocator implements EssentialModLocator_Forge { private Stream paths; + @Override public Iterable scanMods(Stream paths) { this.paths = paths; return scanMods().stream().map(it -> (ModFile) it.file())::iterator; diff --git a/stage2/modlauncher9/forge49/build.gradle b/stage2/modlauncher9/forge49/build.gradle index 60cf93e..9954c08 100644 --- a/stage2/modlauncher9/forge49/build.gradle +++ b/stage2/modlauncher9/forge49/build.gradle @@ -4,6 +4,7 @@ repositories { dependencies { compileOnly(parent.project("compatibility")) + compileOnly(parent.project("forge37")) compileOnly("net.minecraftforge:fmlloader:1.20.4-49.0.38") } diff --git a/stage2/modlauncher9/forge49/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_49_0_38_ModLocator.java b/stage2/modlauncher9/forge49/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_49_0_38_ModLocator.java index d334005..466a3ca 100644 --- a/stage2/modlauncher9/forge49/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_49_0_38_ModLocator.java +++ b/stage2/modlauncher9/forge49/src/main/java/gg/essential/loader/stage2/modlauncher/Forge_49_0_38_ModLocator.java @@ -7,9 +7,10 @@ import java.util.List; import java.util.stream.Stream; -public class Forge_49_0_38_ModLocator extends AbstractModProvider implements EssentialModLocator { +public class Forge_49_0_38_ModLocator extends AbstractModProvider implements EssentialModLocator_Forge { private Stream paths; + @Override public Iterable scanMods(Stream paths) { this.paths = paths; return scanMods().stream().map(it -> (ModFile) it.file())::iterator; diff --git a/stage2/modlauncher9/modlauncher10/build.gradle b/stage2/modlauncher9/modlauncher10/build.gradle index 3f028cc..9e80653 100644 --- a/stage2/modlauncher9/modlauncher10/build.gradle +++ b/stage2/modlauncher9/modlauncher10/build.gradle @@ -11,7 +11,4 @@ dependencies { compileOnly("cpw.mods:securejarhandler:2.0.3") compileOnly("net.sf.jopt-simple:jopt-simple:5.0.4") compileOnly("org.jetbrains:annotations:23.0.0") - - compileOnly("org.apache.logging.log4j:log4j-api:2.8.1") - compileOnly("net.minecraftforge:fmlloader:1.19-41.0.64") } \ No newline at end of file diff --git a/stage2/modlauncher9/modlauncher10/src/main/java/gg/essential/loader/stage2/modlauncher/ML10CompatibilityLayer.java b/stage2/modlauncher9/modlauncher10/src/main/java/gg/essential/loader/stage2/modlauncher/ML10CompatibilityLayer.java index ec7189b..d8725c9 100644 --- a/stage2/modlauncher9/modlauncher10/src/main/java/gg/essential/loader/stage2/modlauncher/ML10CompatibilityLayer.java +++ b/stage2/modlauncher9/modlauncher10/src/main/java/gg/essential/loader/stage2/modlauncher/ML10CompatibilityLayer.java @@ -4,29 +4,9 @@ import java.util.jar.Manifest; -import static gg.essential.loader.stage2.Utils.hasClass; - public class ML10CompatibilityLayer implements CompatibilityLayer { @Override public Manifest getManifest(SecureJar jar) { return jar.moduleDataProvider().getManifest(); } - - @Override - public EssentialModLocator makeModLocator() { - String version; - if (!hasClass("net.minecraftforge.fml.loading.moddiscovery.AbstractJarFileModLocator")) { - version = "49_0_38"; - } else { - version = "41_0_34"; - } - try { - String clsName = "gg.essential.loader.stage2.modlauncher.Forge_" + version + "_ModLocator"; - return (EssentialModLocator) Class.forName(clsName) - .getDeclaredConstructor() - .newInstance(); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } } diff --git a/stage2/modlauncher9/modlauncher11/build.gradle b/stage2/modlauncher9/modlauncher11/build.gradle new file mode 100644 index 0000000..8df20f6 --- /dev/null +++ b/stage2/modlauncher9/modlauncher11/build.gradle @@ -0,0 +1,14 @@ +repositories { + maven { url "https://maven.neoforged.net/releases" } +} + +dependencies { + compileOnly(project(":stage2:common")) + compileOnly(parent.project("compatibility")) + + compileOnly("cpw.mods:modlauncher:11.0.2") + // modlauncher uses these in its api but does not declare them as such + compileOnly("cpw.mods:securejarhandler:3.0.4") + compileOnly("net.sf.jopt-simple:jopt-simple:5.0.4") + compileOnly("org.jetbrains:annotations:23.0.0") +} diff --git a/stage2/modlauncher9/modlauncher11/src/main/java/gg/essential/loader/stage2/modlauncher/ML11CompatibilityLayer.java b/stage2/modlauncher9/modlauncher11/src/main/java/gg/essential/loader/stage2/modlauncher/ML11CompatibilityLayer.java new file mode 100644 index 0000000..e71d016 --- /dev/null +++ b/stage2/modlauncher9/modlauncher11/src/main/java/gg/essential/loader/stage2/modlauncher/ML11CompatibilityLayer.java @@ -0,0 +1,39 @@ +package gg.essential.loader.stage2.modlauncher; + +import cpw.mods.jarhandling.JarContents; +import cpw.mods.jarhandling.JarMetadata; +import cpw.mods.jarhandling.SecureJar; +import gg.essential.loader.stage2.util.Lazy; + +import java.nio.file.Path; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.jar.Manifest; + +public class ML11CompatibilityLayer implements CompatibilityLayer { + @Override + public SecureJar newSecureJarWithCustomMetadata(BiFunction, JarMetadata, JarMetadata> metadataWrapper, Path path) { + SecureJar[] jarHolder = new SecureJar[1]; + JarContents contents = JarContents.of(path); + JarMetadata metadata = JarMetadata.from(contents); + metadata = metadataWrapper.apply(new Lazy<>(() -> jarHolder[0]), metadata); + SecureJar jar = SecureJar.from(contents, metadata); + jarHolder[0] = jar; + return jar; + } + + @Override + public Manifest getManifest(SecureJar jar) { + return jar.moduleDataProvider().getManifest(); + } + + @Override + public Set getPackages(SecureJar secureJar) { + return secureJar.moduleDataProvider().descriptor().packages(); + } + + @Override + public JarMetadata getJarMetadata(SecureJar secureJar, Path path) { + return JarMetadata.from(JarContents.of(path)); + } +} diff --git a/stage2/modlauncher9/neoforge1/build.gradle b/stage2/modlauncher9/neoforge1/build.gradle new file mode 100644 index 0000000..6a3d769 --- /dev/null +++ b/stage2/modlauncher9/neoforge1/build.gradle @@ -0,0 +1,10 @@ +repositories { + maven { url "https://maven.neoforged.net/releases" } +} + +dependencies { + compileOnly(parent.project("compatibility")) + + compileOnly("net.neoforged.fancymodloader:loader:1.0.0") + compileOnly("cpw.mods:securejarhandler:2.1.23") +} diff --git a/stage2/modlauncher9/neoforge1/src/main/java/gg/essential/loader/stage2/modlauncher/EssentialModLocator_NeoForge.java b/stage2/modlauncher9/neoforge1/src/main/java/gg/essential/loader/stage2/modlauncher/EssentialModLocator_NeoForge.java new file mode 100644 index 0000000..a3ab658 --- /dev/null +++ b/stage2/modlauncher9/neoforge1/src/main/java/gg/essential/loader/stage2/modlauncher/EssentialModLocator_NeoForge.java @@ -0,0 +1,38 @@ +package gg.essential.loader.stage2.modlauncher; + +import cpw.mods.jarhandling.SecureJar; +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.fml.loading.moddiscovery.ModFile; +import net.neoforged.fml.loading.moddiscovery.ModValidator; + +import java.lang.reflect.Field; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; + +public interface EssentialModLocator_NeoForge extends EssentialModLocator { + Iterable scanMods(Stream paths); + + @Override + default boolean injectMods(List modJars) throws ReflectiveOperationException { + Field modValidatorField = FMLLoader.class.getDeclaredField("modValidator"); + modValidatorField.setAccessible(true); + ModValidator modValidator = (ModValidator) modValidatorField.get(null); + + if (modValidator == null) { + return false; + } + + Field candidateModsField = ModValidator.class.getDeclaredField("candidateMods"); + candidateModsField.setAccessible(true); + @SuppressWarnings("unchecked") + List modFiles = (List) candidateModsField.get(modValidator); + + for (ModFile modFile : this.scanMods(modJars.stream().map(SecureJar::getPrimaryPath))) { + modFile.identifyMods(); + modFiles.add(modFile); + } + + return true; + } +} diff --git a/stage2/modlauncher9/neoforge1/src/main/java/gg/essential/loader/stage2/modlauncher/NeoForge_1_0_0_ModLocator.java b/stage2/modlauncher9/neoforge1/src/main/java/gg/essential/loader/stage2/modlauncher/NeoForge_1_0_0_ModLocator.java new file mode 100644 index 0000000..c4641d0 --- /dev/null +++ b/stage2/modlauncher9/neoforge1/src/main/java/gg/essential/loader/stage2/modlauncher/NeoForge_1_0_0_ModLocator.java @@ -0,0 +1,24 @@ +package gg.essential.loader.stage2.modlauncher; + +import net.neoforged.fml.loading.moddiscovery.AbstractJarFileModProvider; +import net.neoforged.fml.loading.moddiscovery.ModFile; + +import java.nio.file.Path; +import java.util.Map; +import java.util.stream.Stream; + +public class NeoForge_1_0_0_ModLocator extends AbstractJarFileModProvider implements EssentialModLocator_NeoForge { + @Override + public void initArguments(Map map) { + } + + @Override + public Iterable scanMods(Stream paths) { + return paths.map(this::createMod).map(it -> (ModFile) it.file())::iterator; + } + + @Override + public String name() { + return "essential-loader"; + } +} diff --git a/stage2/modlauncher9/neoforge4/build.gradle b/stage2/modlauncher9/neoforge4/build.gradle new file mode 100644 index 0000000..f1a5278 --- /dev/null +++ b/stage2/modlauncher9/neoforge4/build.gradle @@ -0,0 +1,11 @@ +repositories { + maven { url "https://maven.neoforged.net/releases" } +} + +dependencies { + compileOnly(parent.project("compatibility")) + compileOnly(parent.project("neoforge1")) + + compileOnly("net.neoforged.fancymodloader:loader:4.0.0") + compileOnly("cpw.mods:securejarhandler:3.0.4") +} diff --git a/stage2/modlauncher9/neoforge4/src/main/java/gg/essential/loader/stage2/modlauncher/NeoForge_4_0_0_ModLocator.java b/stage2/modlauncher9/neoforge4/src/main/java/gg/essential/loader/stage2/modlauncher/NeoForge_4_0_0_ModLocator.java new file mode 100644 index 0000000..58a9eda --- /dev/null +++ b/stage2/modlauncher9/neoforge4/src/main/java/gg/essential/loader/stage2/modlauncher/NeoForge_4_0_0_ModLocator.java @@ -0,0 +1,17 @@ +package gg.essential.loader.stage2.modlauncher; + +import cpw.mods.jarhandling.JarContents; +import net.neoforged.fml.loading.moddiscovery.ModFile; +import net.neoforged.fml.loading.moddiscovery.readers.JarModsDotTomlModFileReader; +import net.neoforged.neoforgespi.locating.ModFileDiscoveryAttributes; + +import java.nio.file.Path; +import java.util.stream.Stream; + +public class NeoForge_4_0_0_ModLocator implements EssentialModLocator_NeoForge { + @Override + public Iterable scanMods(Stream paths) { + JarModsDotTomlModFileReader modFileReader = new JarModsDotTomlModFileReader(); + return paths.map(it -> (ModFile) modFileReader.read(JarContents.of(it), ModFileDiscoveryAttributes.DEFAULT))::iterator; + } +} diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/DescriptorRewritingJarMetadata.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/DescriptorRewritingJarMetadata.java index ef21637..b562d2b 100644 --- a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/DescriptorRewritingJarMetadata.java +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/DescriptorRewritingJarMetadata.java @@ -2,6 +2,8 @@ import cpw.mods.jarhandling.JarMetadata; import cpw.mods.jarhandling.SecureJar; +import gg.essential.loader.stage2.modlauncher.CompatibilityLayer; +import gg.essential.loader.stage2.util.Lazy; import java.lang.module.ModuleDescriptor; @@ -12,13 +14,13 @@ * a lookup table. */ public class DescriptorRewritingJarMetadata implements JarMetadata { - private final SecureJar secureJar; private final JarMetadata delegate; + private final JarMetadata newPkgsMeta; private ModuleDescriptor descriptor; - public DescriptorRewritingJarMetadata(SecureJar secureJar, JarMetadata delegate) { - this.secureJar = secureJar; + public DescriptorRewritingJarMetadata(JarMetadata delegate, JarMetadata newPkgsMeta) { this.delegate = delegate; + this.newPkgsMeta = newPkgsMeta; } @Override @@ -36,7 +38,7 @@ public ModuleDescriptor descriptor() { if (this.descriptor == null) { ModuleDescriptor org = delegate.descriptor(); ModuleDescriptor.Builder builder = ModuleDescriptor.newModule(org.name(), org.modifiers()); - builder.packages(secureJar.getPackages()); + builder.packages(newPkgsMeta.descriptor().packages()); if (!org.isAutomatic()) { org.requires().forEach(builder::requires); org.exports().forEach(builder::exports); diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java index 634635c..15cce21 100644 --- a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/EssentialTransformationService.java @@ -7,10 +7,6 @@ import gg.essential.loader.stage2.modlauncher.EssentialModLocator; import gg.essential.loader.stage2.util.KFFMerger; import gg.essential.loader.stage2.util.SortedJarOrPathList; -import net.minecraftforge.fml.loading.FMLLoader; -import net.minecraftforge.fml.loading.moddiscovery.ModFile; -import net.minecraftforge.fml.loading.moddiscovery.ModValidator; -import net.minecraftforge.forgespi.locating.IModFile; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; @@ -24,18 +20,21 @@ import java.util.Map; import java.util.Set; +import static gg.essential.loader.stage2.Utils.hasClass; + public class EssentialTransformationService implements ITransformationService { private static final Logger LOGGER = LogManager.getLogger(EssentialTransformationService.class); private static final Map COMPATIBILITY_IMPLEMENTATIONS = Map.of( - "9.", "ML9CompatibilityLayer", - "10.", "ML10CompatibilityLayer" + "11.", "ML11CompatibilityLayer", + "10.", "ML10CompatibilityLayer", + "9.", "ML9CompatibilityLayer" ); private static CompatibilityLayer compatibilityLayer; private final Path gameDir; private final List pluginJars = new ArrayList<>(); private final List gameJars = new ArrayList<>(); - private final KFFMerger kffMerger = new KFFMerger(); + private KFFMerger kffMerger; private EssentialModLocator modLocator; private boolean modsInjected; @@ -44,7 +43,10 @@ public EssentialTransformationService(Path gameDir) { } public void addToClasspath(final Path path) { - final SecureJar jar = SecureJar.from(j -> new SelfRenamingJarMetadata(j, path, determineLayer(j)), path); + final SecureJar jar = compatibilityLayer.newSecureJarWithCustomMetadata( + (j, metadata) -> new SelfRenamingJarMetadata(compatibilityLayer, j, metadata), + path + ); if (this.kffMerger.addKotlinJar(path, jar)) { return; } @@ -57,7 +59,7 @@ public void addToClasspath(final Path path) { private static IModuleLayerManager.Layer determineLayer(SecureJar jar) { final String modType = compatibilityLayer.getManifest(jar).getMainAttributes().getValue("FMLModType"); - if (IModFile.Type.LANGPROVIDER.name().equals(modType) || IModFile.Type.LIBRARY.name().equals(modType)) { + if ("LANGPROVIDER".equals(modType) || "LIBRARY".equals(modType)) { return IModuleLayerManager.Layer.PLUGIN; } else { return IModuleLayerManager.Layer.GAME; @@ -73,7 +75,8 @@ private static IModuleLayerManager.Layer determineLayer(SecureJar jar) { public void initialize(IEnvironment environment) { String modLauncherVersion = environment.getProperty(IEnvironment.Keys.MLIMPL_VERSION.get()).orElseThrow(); compatibilityLayer = findCompatibilityLayerImpl(modLauncherVersion); - modLocator = compatibilityLayer.makeModLocator(); + modLocator = findModLocatorImpl(); + kffMerger = new KFFMerger(compatibilityLayer); } @SuppressWarnings("unchecked") @@ -96,6 +99,42 @@ private CompatibilityLayer findCompatibilityLayerImpl(String mlVersion) { } } + 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"; + } else { + version = "1_0_0"; + } + } else { + loader = "Forge"; + if (hasClass("net.minecraftforge.forgespi.locating.IModLocator$ModFileOrException")) { + if (!hasClass("net.minecraftforge.fml.loading.moddiscovery.AbstractJarFileModLocator")) { + version = "49_0_38"; + } else { + version = "41_0_34"; + } + } else { + if (hasClass("net.minecraftforge.fml.loading.moddiscovery.AbstractJarFileModLocator")) { + version = "40_1_60"; + } else { + version = "37_0_0"; + } + } + } + try { + String clsName = "gg.essential.loader.stage2.modlauncher." + loader + "_" + version + "_ModLocator"; + return (EssentialModLocator) Class.forName(clsName) + .getDeclaredConstructor() + .newInstance(); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + @Override public void onLoad(IEnvironment env, Set otherServices) { } @@ -125,7 +164,7 @@ private void configureLayerToBeSortedByVersion(IModuleLayerManager.Layer layer) (Map>) layersField.get(layerManager); layers.compute(layer, (__, list) -> { - SortedJarOrPathList sortedList = new SortedJarOrPathList(kffMerger::maybeMergeInto); + SortedJarOrPathList sortedList = new SortedJarOrPathList(compatibilityLayer, kffMerger::maybeMergeInto); if (list != null) { sortedList.addAll(list); } @@ -149,25 +188,7 @@ private void configureLayerToBeSortedByVersion(IModuleLayerManager.Layer layer) */ private boolean injectMods() { try { - Field modValidatorField = FMLLoader.class.getDeclaredField("modValidator"); - modValidatorField.setAccessible(true); - ModValidator modValidator = (ModValidator) modValidatorField.get(null); - - if (modValidator == null) { - return false; - } - - Field candidateModsField = ModValidator.class.getDeclaredField("candidateMods"); - candidateModsField.setAccessible(true); - @SuppressWarnings("unchecked") - List modFiles = (List) candidateModsField.get(modValidator); - - for (ModFile modFile : this.modLocator.scanMods(this.gameJars.stream().map(SecureJar::getPrimaryPath))) { - modFile.identifyMods(); - modFiles.add(modFile); - } - - return true; + return modLocator.injectMods(gameJars); } catch (Throwable e) { LOGGER.error("Error injecting into mod list:", e); return false; @@ -179,8 +200,30 @@ public List beginScanning(IEnvironment environment) { // Forge makes available the MC version in its `initialize` method, the first of our methods which we can be // sure is called after Forge's method is this one, so this is the earliest point at which we can load essential // proper. - String mcVersion = "forge_" + FMLLoader.versionInfo().mcVersion(); - ActualEssentialLoader essentialLoader = new ActualEssentialLoader(gameDir, mcVersion, this); + Class FMLLoader; + String loader; + try { + FMLLoader = Class.forName("net.minecraftforge.fml.loading.FMLLoader"); + loader = "forge"; + } catch (ClassNotFoundException e1) { + try { + FMLLoader = Class.forName("net.neoforged.fml.loading.FMLLoader"); + loader = "neoforge"; + } catch (ClassNotFoundException e2) { + e2.addSuppressed(e1); + throw new RuntimeException(e2); + } + } + String mcVersion; + try { + Object versionInfo = FMLLoader.getMethod("versionInfo").invoke(null); + mcVersion = (String) versionInfo.getClass().getMethod("mcVersion").invoke(versionInfo); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + String gameVersion = loader + "_" + mcVersion; + + ActualEssentialLoader essentialLoader = new ActualEssentialLoader(gameDir, gameVersion, this); try { essentialLoader.load(); } catch (IOException e) { diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/SelfRenamingJarMetadata.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/SelfRenamingJarMetadata.java index aa5291a..cf3e6f4 100644 --- a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/SelfRenamingJarMetadata.java +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/SelfRenamingJarMetadata.java @@ -4,18 +4,21 @@ import cpw.mods.jarhandling.SecureJar; import cpw.mods.modlauncher.Launcher; import cpw.mods.modlauncher.api.IModuleLayerManager; +import gg.essential.loader.stage2.modlauncher.CompatibilityLayer; import gg.essential.loader.stage2.util.DelegatingJarMetadata; +import gg.essential.loader.stage2.util.Lazy; import gg.essential.loader.stage2.util.SortedJarOrPathList; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.lang.reflect.Field; -import java.nio.file.Path; import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import java.util.Set; +import static gg.essential.loader.stage2.util.KFFMerger.isJarJarKff; + /** * A jar metadata which takes the name of another jar with the same packages in the same layer. * @@ -30,13 +33,13 @@ public class SelfRenamingJarMetadata extends DelegatingJarMetadata { private static final Logger LOGGER = LogManager.getLogger(SelfRenamingJarMetadata.class); private static final ThreadLocal RE_ENTRANCE_LOCK = ThreadLocal.withInitial(() -> false); - private final SecureJar secureJar; - private final IModuleLayerManager.Layer layer; + private final CompatibilityLayer compatibilityLayer; + private final Lazy secureJar; - public SelfRenamingJarMetadata(SecureJar secureJar, Path path, IModuleLayerManager.Layer layer) { - super(JarMetadata.from(secureJar, path)); + public SelfRenamingJarMetadata(CompatibilityLayer compatibilityLayer, Lazy secureJar, JarMetadata delegate) { + super(delegate); + this.compatibilityLayer = compatibilityLayer; this.secureJar = secureJar; - this.layer = layer; } @Override @@ -47,7 +50,7 @@ public String name() { RE_ENTRANCE_LOCK.set(true); } String defaultName = delegate.name(); - Set ourPackages = secureJar.getPackages(); + Set ourPackages = compatibilityLayer.getPackages(secureJar.get()); try { for (SecureJar otherJar : getLayerJars()) { String otherModuleName; @@ -56,10 +59,19 @@ public String name() { } catch (SelfRenamingReEntranceException ignored) { continue; } - if (otherJar.getPackages().stream().anyMatch(ourPackages::contains)) { + Set otherPackages = compatibilityLayer.getPackages(otherJar); + if (otherPackages.stream().anyMatch(ourPackages::contains)) { LOGGER.debug("Found existing module with name {}, renaming {} to match.", otherModuleName, defaultName); return otherModuleName; } + // Special case for fully-JarJar-reliant KFF which no longer contains any code itself and doesn't + // declare its module name in its manifest either. + // We still need to make the KFF we ship to use the same module name though, because otherwise it'll + // be loaded and we'll end up with two modules exporting Kotlin. + if (defaultName.equals("thedarkcolour.kotlinforforge") && otherPackages.isEmpty() && isJarJarKff(otherJar)) { + LOGGER.debug("Found existing JarJar KFF with name {}, renaming {} to match.", otherModuleName, defaultName); + return otherModuleName; + } } } catch (Throwable e) { LOGGER.error("Exception occurred while trying to self-rename module " + defaultName + ": ", e); @@ -75,7 +87,16 @@ private List getLayerElements() throws Throwable { IModuleLayerManager layerManager = Launcher.INSTANCE.findLayerManager().orElseThrow(); Field layersField = layerManager.getClass().getDeclaredField("layers"); layersField.setAccessible(true); - return ((EnumMap>) layersField.get(layerManager)).get(this.layer); + EnumMap> map = + (EnumMap>) layersField.get(layerManager); + IModuleLayerManager.Layer[] layers = IModuleLayerManager.Layer.values(); + for (int i = layers.length - 1; i >= 0; i--) { + List elements = map.get(layers[i]); + if (elements != null) { + return elements; + } + } + throw new RuntimeException("Failed to find current layer?!"); } private List getLayerJars() throws Throwable { diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/modlauncher/ML9CompatibilityLayer.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/modlauncher/ML9CompatibilityLayer.java index 71e7a70..6a19829 100644 --- a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/modlauncher/ML9CompatibilityLayer.java +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/modlauncher/ML9CompatibilityLayer.java @@ -4,29 +4,9 @@ import java.util.jar.Manifest; -import static gg.essential.loader.stage2.Utils.hasClass; - public class ML9CompatibilityLayer implements CompatibilityLayer { @Override public Manifest getManifest(SecureJar jar) { return jar.getManifest(); } - - @Override - public EssentialModLocator makeModLocator() { - String version; - if (hasClass("net.minecraftforge.fml.loading.moddiscovery.AbstractJarFileModLocator")) { - version = "40_1_60"; - } else { - version = "37_0_0"; - } - try { - String clsName = "gg.essential.loader.stage2.modlauncher.Forge_" + version + "_ModLocator"; - return (EssentialModLocator) Class.forName(clsName) - .getDeclaredConstructor() - .newInstance(); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } - } } diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/KFFMerger.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/KFFMerger.java index c136ed0..6a96329 100644 --- a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/KFFMerger.java +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/KFFMerger.java @@ -3,6 +3,7 @@ import cpw.mods.jarhandling.JarMetadata; import cpw.mods.jarhandling.SecureJar; import gg.essential.loader.stage2.DescriptorRewritingJarMetadata; +import gg.essential.loader.stage2.modlauncher.CompatibilityLayer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -63,10 +64,15 @@ public void maybeUpgrade(List injectedJars, int theirVersion) { } } + private final CompatibilityLayer compatibilityLayer; private final Libraries ourCoreJars = new Libraries("Kotlin core"); private final Libraries ourCoroutinesJars = new Libraries("Kotlin Coroutines"); private final Libraries ourSerializationJars = new Libraries("Kotlin Serialization"); + public KFFMerger(CompatibilityLayer compatibilityLayer) { + this.compatibilityLayer = compatibilityLayer; + } + public boolean addKotlinJar(Path sourceFile, SecureJar secureJar) { String fileName = sourceFile.getFileName().toString(); Matcher matcher = JIJ_KOTLIN_FILES.matcher(fileName); @@ -97,15 +103,28 @@ public boolean addKotlinJar(Path sourceFile, SecureJar secureJar) { return true; } - public SecureJar maybeMergeInto(SecureJar secureJar) { + public List maybeMergeInto(SecureJar secureJar) { // Nothing to merge (older Essential version), nothing to do if (ourCoreJars.jars.isEmpty()) { - return secureJar; + return null; + } + + // If this is a KotlinForForge version which uses JarJar to bundle Kotlin (KFF 5+, potentially 4.12), + // we no longer need to merge the Kotlin jars into it, we merely need to add our Kotlin jars to the to-be-loaded + // list too. + if (isJarJarKff(secureJar)) { + LOGGER.info("Looks like KotlinForForge is using JarJar now, all good to go: {}", secureJar); + List allJars = new ArrayList<>(); + allJars.add(secureJar); // keep user-installed jar + allJars.addAll(ourCoreJars.jars); + allJars.addAll(ourCoroutinesJars.jars); + allJars.addAll(ourSerializationJars.jars); + return allJars; } // Only care about a jar if it contains a Kotlin we can overwrite - if (!secureJar.getPackages().contains("kotlin")) { - return secureJar; + if (!compatibilityLayer.getPackages(secureJar).contains("kotlin")) { + return null; } LOGGER.info("Found Kotlin-containing mod {}, checking whether we need to upgrade it..", secureJar); @@ -120,6 +139,13 @@ public SecureJar maybeMergeInto(SecureJar secureJar) { boolean updateCoroutines = theirCoroutinesVersion < ourCoroutinesJars.version; int theirSerializationVersion = updateCore || updateCoroutines ? 0 : ourSerializationJars.version; + // If the jar contains only core but not coroutine libs, then it's not the fat KFF jar but rather KFF is + // using JarJar, and we should be able to find that one later. + if (theirCoreVersion != 0 && theirCoroutinesVersion == 0) { + LOGGER.info("Looks like a standalone Kotlin jar. Keeping as is, we should be finding a JarJar KFF jar."); + return null; + } + List injectedJars = new ArrayList<>(); ourCoreJars.maybeUpgrade(injectedJars, theirCoreVersion); ourCoroutinesJars.maybeUpgrade(injectedJars, theirCoroutinesVersion); @@ -127,11 +153,11 @@ public SecureJar maybeMergeInto(SecureJar secureJar) { if (injectedJars.isEmpty()) { LOGGER.info("All good, no update needed: {}", secureJar); - return secureJar; // all up-to-date, nothing to do + return null; // all up-to-date, nothing to do } try { - JarMetadata orgMeta = JarMetadata.from(secureJar, secureJar.getPrimaryPath()); + JarMetadata orgMeta = compatibilityLayer.getJarMetadata(secureJar, secureJar.getPrimaryPath()); Path tmpFile = Files.createTempFile("kff-updated-kotlin-", "-" + orgMeta.version() + ".jar"); Files.write(tmpFile, EMPTY_ZIP); @@ -169,16 +195,36 @@ public SecureJar maybeMergeInto(SecureJar secureJar) { } } - return SecureJar.from(j -> new DescriptorRewritingJarMetadata(j, orgMeta) { + return List.of(compatibilityLayer.newSecureJarWithCustomMetadata((__, newMeta) -> new DescriptorRewritingJarMetadata(orgMeta, newMeta) { @Override public String name() { // Call the original name from the original SecureJar to allow SelfRenamingJarMetadata to function return secureJar.name(); } - }, tmpFile); + }, tmpFile)); } catch (Throwable t) { LOGGER.fatal("Failed to merge updated Kotlin into " + secureJar + ":", t); - return secureJar; // oh well, guess we'll give it a try as is + return null; // oh well, guess we'll give it a try as is + } + } + + public static boolean isJarJarKff(SecureJar jar) { + try { + Path jarjarPath = jar.getRootPath().resolve("META-INF").resolve("jarjar"); + if (!Files.exists(jarjarPath)) return false; + try (Stream stream = Files.list(jarjarPath)) { + List files = stream + .map(it -> it.getFileName().toString()) + .filter(it -> it.endsWith(".jar")) + .toList(); + // A JarJar-using KotlinForForge jar can be recognized by the fact that it bundles both the KFF mod as + // well as the Kotlin Standard Library + return files.stream().anyMatch(it -> it.startsWith("kffmod-")) + && files.stream().anyMatch(it -> it.startsWith("kotlin-stdlib-")); + } + } catch (Throwable t) { + LOGGER.error("Failed to determine version of potential KFF jar at " + jar + ":", t); + return false; } } diff --git a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/SortedJarOrPathList.java b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/SortedJarOrPathList.java index 6b8175f..93594ca 100644 --- a/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/SortedJarOrPathList.java +++ b/stage2/modlauncher9/src/main/java/gg/essential/loader/stage2/util/SortedJarOrPathList.java @@ -3,6 +3,7 @@ import cpw.mods.jarhandling.JarMetadata; import cpw.mods.jarhandling.SecureJar; import cpw.mods.modlauncher.api.NamedPath; +import gg.essential.loader.stage2.modlauncher.CompatibilityLayer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.maven.artifact.versioning.ArtifactVersion; @@ -14,9 +15,11 @@ import java.util.Collection; import java.util.Comparator; import java.util.IdentityHashMap; +import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.jar.Manifest; /** * List which keeps its JarOrPath elements sorted by their module version, latest first. @@ -35,12 +38,15 @@ public class SortedJarOrPathList extends ArrayList { private final Comparator COMPARATOR = Comparator.comparing(pathOrJar -> versionCache.computeIfAbsent(pathOrJar, this::getVersion)).reversed(); + private final CompatibilityLayer compatibilityLayer; + // This does not actually have anything to do with the original functionality of this class but we needed an entry // point for replacing one jar (KFF with old Kotlin) with another jar (same KFF but with newer Kotlin merged into // it) and this class is perfect for that. - private final Function substitute; + private final Function> substitute; - public SortedJarOrPathList(Function substitute) { + public SortedJarOrPathList(CompatibilityLayer compatibilityLayer, Function> substitute) { + this.compatibilityLayer = compatibilityLayer; this.substitute = substitute; } @@ -50,7 +56,40 @@ private ArtifactVersion getVersion(Object pathOrJar) { JarMetadata metadata = getMetadata(jar); if (metadata == null) return FALLBACK_VERSION; String version = metadata.version(); - if (version == null) return FALLBACK_VERSION; + + // Some revisions of ModLauncher have a bug where they simply call `toString` on `Optional`, resulting + // in versions being reported as the string "Optional.empty" or "Optional[1.2.3]" instead of `null` or "1.2.3". + // See https://github.com/McModLauncher/securejarhandler/blob/7cd8481364d73bacecf2b608479c6b903bff7f6c/src/main/java/cpw/mods/jarhandling/impl/ModuleJarMetadata.java#L137 + // We need to unwrap those to get at the real version. + if (version != null && version.equals("Optional.empty")) { + version = null; + } else if (version != null && version.startsWith("Optional[")) { + version = version.substring("Optional[".length(), version.length() - 1); + } + + // Additionally, when ModuleJarMetadata is used (not entirely sure when that's the case, at the very least the + // jar must have a `module-info.class` but mods may also use ModJarMetadata instead), ModLauncher only looks at + // the version declared in the `module-info.class`. For most jars that version is `null` though because it + // requires extra setup in Gradle which most people don't do. + // We need a version for correct sorting though, so we'll try to find one ourselves. + if (version == null) { + Manifest manifest = compatibilityLayer.getManifest(jar); + if (manifest != null) { + version = manifest.getMainAttributes().getValue("Implementation-Version"); + } + } + // and if that doesn't work (some of the Kotlin libs, e.g. kotlinx-serialization-json-jvm-1.7.3, don't have + // such an attribute), then we'll take a guess based on the file name + if (version == null) { + String name = jar.getPrimaryPath().getFileName().toString(); + if (name.contains("-") && name.endsWith(".jar")) { + version = name.substring(name.lastIndexOf("-") + 1, name.length() - ".jar".length()); + } + } + + if (version == null) { + return FALLBACK_VERSION; + } return new DefaultArtifactVersion(version); } @@ -93,31 +132,30 @@ private JarMetadata getMetadata(SecureJar jar) { @Override public boolean add(Object o) { - boolean changed = super.add(substitute(o)); - sort(COMPARATOR); - return changed; + //noinspection RedundantCollectionOperation + return addAll(List.of(o)); } @Override public boolean addAll(Collection c) { - boolean changed = super.addAll(c.stream().map(this::substitute).toList()); + boolean changed = super.addAll(c.stream().flatMap(it -> substitute(it).stream()).toList()); sort(COMPARATOR); return changed; } - private Object substitute(Object orgPathOrJar) { + private List substitute(Object orgPathOrJar) { SecureJar orgJar = getJar(orgPathOrJar); if (orgJar == null) { - return orgPathOrJar; + return List.of(orgPathOrJar); } - SecureJar newJar = substitute.apply(orgJar); - if (newJar == orgJar) { - return orgPathOrJar; + List newJars = substitute.apply(orgJar); + if (newJars == null) { + return List.of(orgPathOrJar); } if (orgPathOrJar instanceof SecureJar) { - return newJar; + return newJars; } if (pathOrJarConstructor == null) { @@ -137,11 +175,15 @@ private Object substitute(Object orgPathOrJar) { pathOrJarConstructor = (path, jar) -> null; } } - Object newPathOrJar = pathOrJarConstructor.apply(null, newJar); - if (newPathOrJar == null) { - return orgPathOrJar; + List newPathOrJars = new ArrayList<>(newJars.size()); + for (SecureJar newJar : newJars) { + Object newPathOrJar = pathOrJarConstructor.apply(null, newJar); + if (newPathOrJar == null) { + return List.of(orgPathOrJar); + } + newPathOrJars.add(newPathOrJar); } - return newPathOrJar; + return newPathOrJars; } }