From 70ec5b1b164c613c8a9910ba4a42608282a23cf6 Mon Sep 17 00:00:00 2001 From: shartte Date: Fri, 14 Jun 2024 17:04:08 +0200 Subject: [PATCH] Use a TransformingClassLoader in tests and write Data-Driven Enum Extension tests (#163) --- gradle.properties | 2 +- .../asm/enumextension/EnumPrototype.java | 2 +- .../common/asm/enumextension/EnumProxy.java | 5 +- .../enumextension/RuntimeEnumExtender.java | 2 +- .../net/neoforged/fml/loading/FMLLoader.java | 6 +- .../neoforged/fml/loading/LoadingModList.java | 4 +- .../fml/asm/RuntimeEnumExtensionTest.java | 160 --------- .../common/asm/RuntimeDistCleanerTest.java | 39 +- .../RuntimeEnumExtenderTest.java | 334 ++++++++++++++++++ .../neoforged/fml/loading/CoreModTest.java | 39 +- .../neoforged/fml/loading/LauncherTest.java | 41 ++- .../neoforged/fml/loading/ModFileBuilder.java | 65 ---- .../fml/loading/TestEnvironment.java | 17 +- 13 files changed, 439 insertions(+), 277 deletions(-) delete mode 100644 loader/src/test/java/net/neoforged/fml/asm/RuntimeEnumExtensionTest.java create mode 100644 loader/src/test/java/net/neoforged/fml/common/asm/enumextension/RuntimeEnumExtenderTest.java diff --git a/gradle.properties b/gradle.properties index 21a2d71f5..e4e13e86a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ mergetool_version=2.0.0 accesstransformers_version=10.0.1 coremods_version=7.0.3 eventbus_version=7.0.16 -modlauncher_version=10.1.10 +modlauncher_version=11.0.3 securejarhandler_version=3.0.7 bootstraplauncher_version=1.1.8 asm_version=9.5 diff --git a/loader/src/main/java/net/neoforged/fml/common/asm/enumextension/EnumPrototype.java b/loader/src/main/java/net/neoforged/fml/common/asm/enumextension/EnumPrototype.java index 92f4074c5..e5e57c31c 100644 --- a/loader/src/main/java/net/neoforged/fml/common/asm/enumextension/EnumPrototype.java +++ b/loader/src/main/java/net/neoforged/fml/common/asm/enumextension/EnumPrototype.java @@ -106,7 +106,7 @@ static List load(IModInfo mod, Path path) { } return prototypes; } catch (Throwable e) { - ModLoader.addLoadingIssue(ModLoadingIssue.error("fml.modloading.enumextender.loading_error") + ModLoader.addLoadingIssue(ModLoadingIssue.error("fml.modloading.enumextender.loading_error", path) .withAffectedMod(mod) .withCause(e)); return List.of(); diff --git a/loader/src/main/java/net/neoforged/fml/common/asm/enumextension/EnumProxy.java b/loader/src/main/java/net/neoforged/fml/common/asm/enumextension/EnumProxy.java index 98a6e0d58..4aeb2a6a3 100644 --- a/loader/src/main/java/net/neoforged/fml/common/asm/enumextension/EnumProxy.java +++ b/loader/src/main/java/net/neoforged/fml/common/asm/enumextension/EnumProxy.java @@ -38,13 +38,12 @@ public Object getParameter(int idx) { public T getValue() { if (enumValue == null) { try { - var loader = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).getCallerClass().getClassLoader(); - Class.forName(enumClass.getName(), true, loader); + Class.forName(enumClass.getName(), true, enumClass.getClassLoader()); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } - return Objects.requireNonNull(enumValue, "Enum not initialized"); + return Objects.requireNonNull(enumValue, "Enum not initialized. Did you forget to configure the field holding this proxy as a parameter in the enum extension config file?"); } @ApiStatus.Internal diff --git a/loader/src/main/java/net/neoforged/fml/common/asm/enumextension/RuntimeEnumExtender.java b/loader/src/main/java/net/neoforged/fml/common/asm/enumextension/RuntimeEnumExtender.java index eedcd5d36..421cff9d1 100644 --- a/loader/src/main/java/net/neoforged/fml/common/asm/enumextension/RuntimeEnumExtender.java +++ b/loader/src/main/java/net/neoforged/fml/common/asm/enumextension/RuntimeEnumExtender.java @@ -452,7 +452,7 @@ public static void loadEnumPrototypes(Map paths) { .reduce( new HashMap<>(), (map, proto) -> { - map.computeIfAbsent(proto.enumName(), $ -> new ArrayList<>()).add(proto); + map.computeIfAbsent(proto.enumName(), ignored -> new ArrayList<>()).add(proto); return map; }, (protoOne, protoTwo) -> { diff --git a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java index 6fb6f0bb5..1c8a7d51a 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java +++ b/loader/src/main/java/net/neoforged/fml/loading/FMLLoader.java @@ -56,11 +56,7 @@ static void onInitialLoad(IEnvironment environment) throws IncompatibleEnvironme final String version = LauncherVersion.getVersion(); LOGGER.debug(LogMarkers.CORE, "FML {} loading", version); final Package modLauncherPackage = ITransformationService.class.getPackage(); - LOGGER.debug(LogMarkers.CORE, "FML found ModLauncher version : {}", modLauncherPackage.getImplementationVersion()); - if (!modLauncherPackage.isCompatibleWith("4.0")) { - LOGGER.error(LogMarkers.CORE, "Found incompatible ModLauncher specification : {}, version {} from {}", modLauncherPackage.getSpecificationVersion(), modLauncherPackage.getImplementationVersion(), modLauncherPackage.getImplementationVendor()); - throw new IncompatibleEnvironmentException("Incompatible modlauncher found " + modLauncherPackage.getSpecificationVersion()); - } + LOGGER.debug(LogMarkers.CORE, "FML found ModLauncher version : {}", environment.getProperty(IEnvironment.Keys.MLIMPL_VERSION.get()).orElse("unknown")); accessTransformer = ((AccessTransformerService) environment.findLaunchPlugin("accesstransformer").orElseThrow(() -> { LOGGER.error(LogMarkers.CORE, "Access Transformer library is missing, we need this to run"); diff --git a/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java b/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java index d05dde6d9..304ca1cd7 100644 --- a/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java +++ b/loader/src/main/java/net/neoforged/fml/loading/LoadingModList.java @@ -92,9 +92,9 @@ public void addEnumExtenders() { modFiles.stream() .map(ModFileInfo::getMods) .flatMap(List::stream) - .forEach(mod -> mod.getConfig().getConfigElement("enumExtender").ifPresent(file -> { + .forEach(mod -> mod.getConfig().getConfigElement("enumExtensions").ifPresent(file -> { Path path = mod.getOwningFile().getFile().findResource(file); - if (Files.notExists(path)) { + if (!Files.isRegularFile(path)) { ModLoader.addLoadingIssue(ModLoadingIssue.error("fml.modloading.enumextender.file_not_found", path).withAffectedMod(mod)); return; } diff --git a/loader/src/test/java/net/neoforged/fml/asm/RuntimeEnumExtensionTest.java b/loader/src/test/java/net/neoforged/fml/asm/RuntimeEnumExtensionTest.java deleted file mode 100644 index 932857175..000000000 --- a/loader/src/test/java/net/neoforged/fml/asm/RuntimeEnumExtensionTest.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (c) NeoForged and contributors - * SPDX-License-Identifier: LGPL-2.1-only - */ - -package net.neoforged.fml.asm; - -import net.neoforged.fml.loading.LauncherTest; -import org.junit.jupiter.api.Test; - -// FIXME: doesn't actually test anything because there's no transforming classloader (see commented-out code in test mod class) -public class RuntimeEnumExtensionTest extends LauncherTest { - @Test - public void testEnumExtension() throws Exception { - installation.setupProductionClient(); - - installation.buildModJar("enum_ext_test.jar") - .withModsToml(builder -> builder - .unlicensedJavaMod() - .addMod("enumtestmod", "1.0", config -> config.set("enumExtender", "META-INF/enumextender.json"))) - .addTextFile("META-INF/enumextender.json", """ - { - "entries": [ - { - "enum": "enumtestmod/ExtensibleEnum", - "name": "ENUMTESTMOD_PREFIXED_TEST_THING", - "constructor": "(Ljava/lang/String;)V", - "parameters": [ "enumtestmod:prefixed_test_thing" ] - }, - { - "enum": "enumtestmod/ExtensibleEnum", - "name": "ENUMTESTMOD_LAZY_TEST_THING", - "constructor": "(Ljava/lang/String;)V", - "parameters": { - "class": "enumtestmod/TestMod", - "field": "ENUM_PARAMS" - } - }, - { - "enum": "enumtestmod/ExtensibleEnum", - "name": "ENUMTESTMOD_MTH_LAZY_TEST_THING", - "constructor": "(Ljava/lang/String;)V", - "parameters": { - "class": "enumtestmod/TestMod", - "method": "getEnumParameter" - } - }, - { - "enum": "enumtestmod/EnumWithId", - "name": "ENUMTESTMOD_PREFIXED_ID_TEST_THING", - "constructor": "(ILjava/lang/String;)V", - "parameters": [ -1, "enumtestmod:prefixed_id_test_thing" ] - }, - { - "enum": "enumtestmod/EnumWithId", - "name": "ENUMTESTMOD_LAZY_ID_TEST_THING", - "constructor": "(ILjava/lang/String;)V", - "parameters": { - "class": "enumtestmod/TestMod", - "field": "ID_ENUM_PARAMS" - } - }, - { - "enum": "enumtestmod/EnumWithId", - "name": "ENUMTESTMOD_MTH_LAZY_ID_TEST_THING", - "constructor": "(ILjava/lang/String;)V", - "parameters": { - "class": "enumtestmod/TestMod", - "method": "getIdEnumParameter" - } - } - ] - } - """) - .addClass("enumtestmod.ExtensibleEnum", """ - package enumtestmod; - @net.neoforged.fml.common.asm.enumextension.NamedEnum - public enum ExtensibleEnum implements net.neoforged.fml.common.asm.enumextension.IExtensibleEnum { - TEST_THING("test"); - private final String name; - ExtensibleEnum(String name) { - this.name = name; - } - public static net.neoforged.fml.common.asm.enumextension.ExtensionInfo getExtensionInfo() { - return net.neoforged.fml.common.asm.enumextension.ExtensionInfo.nonExtended(ExtensibleEnum.class); - } - } - """) - .addClass("enumtestmod.EnumWithId", """ - package enumtestmod; - @net.neoforged.fml.common.asm.enumextension.IndexedEnum - @net.neoforged.fml.common.asm.enumextension.NamedEnum(1) - public enum EnumWithId implements net.neoforged.fml.common.asm.enumextension.IExtensibleEnum { - TEST_ID_THING(0, "test"); - private final int id; - private final String name; - EnumWithId(int id, String name) { - this.id = id; - this.name = name; - } - public int getId() { - return id; - } - public static net.neoforged.fml.common.asm.enumextension.ExtensionInfo getExtensionInfo() { - return net.neoforged.fml.common.asm.enumextension.ExtensionInfo.nonExtended(EnumWithId.class); - } - } - """) - .addClass("enumtestmod.TestMod", """ - package enumtestmod; - @net.neoforged.fml.common.Mod("enumtestmod") - public class TestMod { - public static final net.neoforged.fml.common.asm.enumextension.EnumProxy ENUM_PARAMS = - new net.neoforged.fml.common.asm.enumextension.EnumProxy<>(ExtensibleEnum.class, "enumtestmod:lazy_test_thing"); - public static final net.neoforged.fml.common.asm.enumextension.EnumProxy ID_ENUM_PARAMS = - new net.neoforged.fml.common.asm.enumextension.EnumProxy<>(EnumWithId.class, -1, "enumtestmod:lazy_id_test_thing"); - public TestMod() { - System.out.println(java.util.Arrays.toString(ExtensibleEnum.values())); - System.out.println(java.util.Arrays.toString(EnumWithId.values())); - /* - System.out.println(ENUM_PARAMS.getValue()); - System.out.println(ID_ENUM_PARAMS.getValue()); - ExtensibleEnum prefixedTestThing = ExtensibleEnum.valueOf("ENUMTESTMOD_PREFIXED_TEST_THING"); - ExtensibleEnum lazyTestThing = ExtensibleEnum.valueOf("ENUMTESTMOD_LAZY_TEST_THING"); - ExtensibleEnum mthLazyTestThing = ExtensibleEnum.valueOf("ENUMTESTMOD_MTH_LAZY_TEST_THING"); - com.google.common.base.Preconditions.checkState(prefixedTestThing.ordinal() == 3); - com.google.common.base.Preconditions.checkState(lazyTestThing.ordinal() == 1); - com.google.common.base.Preconditions.checkState(mthLazyTestThing.ordinal() == 2); - EnumWithId prefixedIdTestThing = EnumWithId.valueOf("ENUMTESTMOD_PREFIXED_ID_TEST_THING"); - EnumWithId lazyIdTestThing = EnumWithId.valueOf("ENUMTESTMOD_LAZY_ID_TEST_THING"); - EnumWithId mthLazyIdTestThing = EnumWithId.valueOf("ENUMTESTMOD_MTH_LAZY_ID_TEST_THING"); - com.google.common.base.Preconditions.checkState(prefixedIdTestThing.ordinal() == 3); - com.google.common.base.Preconditions.checkState(prefixedIdTestThing.getId() == 3); - com.google.common.base.Preconditions.checkState(lazyIdTestThing.ordinal() == 1); - com.google.common.base.Preconditions.checkState(lazyIdTestThing.getId() == 1); - com.google.common.base.Preconditions.checkState(mthLazyIdTestThing.ordinal() == 2); - com.google.common.base.Preconditions.checkState(mthLazyIdTestThing.getId() == 2); - System.out.println(ExtensibleEnum.getExtensionInfo()); - System.out.println(EnumWithId.getExtensionInfo()); - */ - } - public static Object getEnumParameter(int idx, Class type) { - if (idx == 0) { - return type.cast("enumtestmod:mth_lazy_test_thing"); - } - throw new IllegalArgumentException("Unexpected param idx: " + idx); - } - public static Object getIdEnumParameter(int idx, Class type) { - return type.cast(switch (idx) { - case 0 -> -1; - case 1 -> "enumtestmod:mth_lazy_id_test_thing"; - default -> throw new IllegalArgumentException("Unexpected param idx: " + idx); - }); - } - } - """) - .build(); - launchAndLoad("forgeclient"); - } -} diff --git a/loader/src/test/java/net/neoforged/fml/common/asm/RuntimeDistCleanerTest.java b/loader/src/test/java/net/neoforged/fml/common/asm/RuntimeDistCleanerTest.java index 6e1cf0247..20e424a8d 100644 --- a/loader/src/test/java/net/neoforged/fml/common/asm/RuntimeDistCleanerTest.java +++ b/loader/src/test/java/net/neoforged/fml/common/asm/RuntimeDistCleanerTest.java @@ -9,20 +9,13 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.lang.reflect.Field; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Path; import java.util.function.Consumer; import net.neoforged.api.distmarker.Dist; -import net.neoforged.fml.loading.ModFileBuilder; +import net.neoforged.fml.loading.LauncherTest; import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -class RuntimeDistCleanerTest { - @TempDir - Path tempDir; +class RuntimeDistCleanerTest extends LauncherTest { @Test void testStripInterface() throws Exception { transformTestClass(Dist.CLIENT, """ @@ -123,26 +116,20 @@ private void transformTestClass(Dist dist, @Language("java") String classContent } private void transformTestClass(Dist dist, @Language("java") String classContent, Consumer> asserter) throws Exception { - var distCleaner = new RuntimeDistCleaner(); - distCleaner.setDistribution(dist); + if (dist.isClient()) { + installation.setupProductionClient(); + } else { + installation.setupProductionServer(); + } - var modJar = tempDir.resolve("modjar.jar"); - new ModFileBuilder(modJar) + installation.buildModJar("modjar.jar") + .withTestmodModsToml() .addClass("test.Test", classContent) - .withTransform((type, classNode) -> { - var phases = distCleaner.handlesClass(type, false); - for (var phase : phases) { - distCleaner.processClassWithFlags(phase, classNode, type, ""); - } - return classNode; - }) .build(); - try (var cl = new URLClassLoader(new URL[] { - modJar.toUri().toURL() - })) { - var testClass = cl.loadClass("test.Test"); - asserter.accept(testClass); - } + launchAndLoad(dist.isClient() ? "forgeclient" : "forgeserver"); + + var testClass = Class.forName("test.Test", true, gameClassLoader); + asserter.accept(testClass); } } diff --git a/loader/src/test/java/net/neoforged/fml/common/asm/enumextension/RuntimeEnumExtenderTest.java b/loader/src/test/java/net/neoforged/fml/common/asm/enumextension/RuntimeEnumExtenderTest.java new file mode 100644 index 000000000..cd44ec0a5 --- /dev/null +++ b/loader/src/test/java/net/neoforged/fml/common/asm/enumextension/RuntimeEnumExtenderTest.java @@ -0,0 +1,334 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.fml.common.asm.enumextension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.function.Consumer; +import net.neoforged.fml.ModLoadingException; +import net.neoforged.fml.loading.LauncherTest; +import net.neoforged.fml.loading.ModsTomlBuilder; +import org.junit.jupiter.api.Test; + +class RuntimeEnumExtenderTest extends LauncherTest { + @Test + void testMissingPath() throws Exception { + installation.setupProductionClient(); + installation.buildModJar("testmod.jar") + .withModsToml(getModsTomlBuilderConsumer("xyz")) + .build(); + + var e = assertThrows(ModLoadingException.class, () -> launchAndLoad("forgeclient")); + assertThat(getTranslatedIssues(e.getIssues())).containsOnly( + "ERROR: Enum extender file xyz, provided by mod testmod, does not exist"); + } + + @Test + > void testExtendEnum() throws Exception { + installation.setupProductionClient(); + installation.buildModJar("testmod.jar") + .withModsToml(getModsTomlBuilderConsumer("extensions.json")) + .addTextFile("extensions.json", """ + { + "entries": [ + { + "enum": "testmod/SomeEnum", + "name": "TESTMOD_NEW_CONSTANT", + "constructor": "()V", + "parameters": [] + } + ] + } + """) + .addClass("testmod.SomeEnum", """ + import net.neoforged.fml.common.asm.enumextension.IExtensibleEnum; + import net.neoforged.fml.common.asm.enumextension.ExtensionInfo; + enum SomeEnum implements IExtensibleEnum { + LITERAL; + public static ExtensionInfo getExtensionInfo() { + return ExtensionInfo.nonExtended(SomeEnum.class); + } + } + """) + .build(); + + launchAndLoad("forgeclient"); + + Class enumClass = getEnumClass("testmod.SomeEnum"); + + assertThat(enumClass).hasSuperclass(Enum.class); + assertThat(enumClass.getEnumConstants()).extracting(Enum::name).containsExactly( + "LITERAL", "TESTMOD_NEW_CONSTANT"); + assertThat(enumClass.getEnumConstants()).extracting(Enum::ordinal).containsExactly( + 0, 1); + assertThat(Enum.valueOf(enumClass, "LITERAL")).isInstanceOf(enumClass); + assertThat(Enum.valueOf(enumClass, "TESTMOD_NEW_CONSTANT")).isInstanceOf(enumClass); + } + + @Test + void testEnumProxy() throws Exception { + installation.setupProductionClient(); + + installation.buildModJar("enum_ext_test.jar") + .withModsToml(getModsTomlBuilderConsumer("extensions.json")) + .addTextFile("extensions.json", """ + { + "entries": [ + { + "enum": "testmod/NoArgEnum", + "name": "TESTMOD_NEW_CONSTANT", + "constructor": "()V", + "parameters": { + "class": "testmod/TestMod", + "field": "NO_ARG" + } + }, + { + "enum": "testmod/StringArgEnum", + "name": "TESTMOD_NEW_CONSTANT", + "constructor": "(Ljava/lang/String;)V", + "parameters": { + "class": "testmod/TestMod", + "field": "WITH_ARG" + } + } + ] + } + """) + .addClass("testmod.NoArgEnum", """ + @net.neoforged.fml.common.asm.enumextension.NamedEnum + public enum NoArgEnum implements net.neoforged.fml.common.asm.enumextension.IExtensibleEnum { + TEST_THING; + public static net.neoforged.fml.common.asm.enumextension.ExtensionInfo getExtensionInfo() { + return net.neoforged.fml.common.asm.enumextension.ExtensionInfo.nonExtended(NoArgEnum.class); + } + } + """) + .addClass("testmod.StringArgEnum", """ + @net.neoforged.fml.common.asm.enumextension.NamedEnum + public enum StringArgEnum implements net.neoforged.fml.common.asm.enumextension.IExtensibleEnum { + TEST_THING("test"); + private final String name; + StringArgEnum(String name) { + this.name = name; + } + public static net.neoforged.fml.common.asm.enumextension.ExtensionInfo getExtensionInfo() { + return net.neoforged.fml.common.asm.enumextension.ExtensionInfo.nonExtended(StringArgEnum.class); + } + } + """) + .addClass("testmod.TestMod", """ + import net.neoforged.fml.common.asm.enumextension.EnumProxy; + @net.neoforged.fml.common.Mod("testmod") + public class TestMod { + public static final EnumProxy NO_ARG = + new EnumProxy<>(NoArgEnum.class, "testmod:lazy_added"); + public static final EnumProxy WITH_ARG = + new EnumProxy<>(StringArgEnum.class, "testmod:lazy_added"); + } + """) + .build(); + launchAndLoad("forgeclient"); + + var noArgEnum = getEnumClass("testmod.NoArgEnum"); + var stringArgEnum = getEnumClass("testmod.StringArgEnum"); + + var testModClass = Class.forName("testmod.TestMod", true, gameClassLoader); + var noArg = (EnumProxy) testModClass.getField("NO_ARG").get(null); + assertThat(noArg.getValue()).isInstanceOf(noArgEnum); + + var stringArg = (EnumProxy) testModClass.getField("WITH_ARG").get(null); + assertThat(stringArg.getValue()).isInstanceOf(stringArgEnum); + } + + @Test + void testNoArgEnumExtension() throws Exception { + installation.setupProductionClient(); + + installation.buildModJar("enum_ext_test.jar") + .withModsToml(builder -> builder + .unlicensedJavaMod() + .addMod("enumtestmod", "1.0", config -> config.set("enumExtensions", "META-INF/enumextensions.json"))) + .addTextFile("META-INF/enumextensions.json", """ + { + "entries": [ + { + "enum": "enumtestmod/ExtensibleEnum", + "name": "ENUMTESTMOD_PREFIXED_TEST_THING", + "constructor": "(Ljava/lang/String;)V", + "parameters": [ "enumtestmod:prefixed_test_thing" ] + }, + { + "enum": "enumtestmod/ExtensibleEnum", + "name": "ENUMTESTMOD_MTH_LAZY_TEST_THING", + "constructor": "(Ljava/lang/String;)V", + "parameters": { + "class": "enumtestmod/TestMod", + "method": "getEnumParameter" + } + } + ] + } + """) + .addClass("enumtestmod.ExtensibleEnum", """ + package enumtestmod; + @net.neoforged.fml.common.asm.enumextension.NamedEnum + public enum ExtensibleEnum implements net.neoforged.fml.common.asm.enumextension.IExtensibleEnum { + TEST_THING("test"); + private final String name; + ExtensibleEnum(String name) { + this.name = name; + } + public static net.neoforged.fml.common.asm.enumextension.ExtensionInfo getExtensionInfo() { + return net.neoforged.fml.common.asm.enumextension.ExtensionInfo.nonExtended(ExtensibleEnum.class); + } + } + """) + .addClass("enumtestmod.TestMod", """ + import net.neoforged.fml.common.asm.enumextension.EnumProxy; + public class TestMod { + public static Object getEnumParameter(int idx, Class type) { + if (idx == 0) { + return type.cast("enumtestmod:mth_lazy_test_thing"); + } + throw new IllegalArgumentException("Unexpected param idx: " + idx); + } + public static Object getIdEnumParameter(int idx, Class type) { + return type.cast(switch (idx) { + case 0 -> -1; + case 1 -> "enumtestmod:mth_lazy_id_test_thing"; + default -> throw new IllegalArgumentException("Unexpected param idx: " + idx); + }); + } + } + """) + .build(); + launchAndLoad("forgeclient"); + + var extensibleEnum = getEnumClass("enumtestmod.ExtensibleEnum"); + assertThat(extensibleEnum.getEnumConstants()).extracting(Enum::name).containsExactly( + "TEST_THING", + "ENUMTESTMOD_MTH_LAZY_TEST_THING", + "ENUMTESTMOD_PREFIXED_TEST_THING"); + assertThat(extensibleEnum.getEnumConstants()).extracting(Enum::ordinal).containsExactly( + 0, + 1, + 2); + + var extensionInfo = getExtensionInfo(extensibleEnum); + assertTrue(extensionInfo.extended()); + assertEquals(1, extensionInfo.vanillaCount()); + assertEquals(3, extensionInfo.totalCount()); + } + + @Test + void testIdArgEnumExtension() throws Exception { + installation.setupProductionClient(); + + installation.buildModJar("enum_ext_test.jar") + .withModsToml(builder -> builder + .unlicensedJavaMod() + .addMod("enumtestmod", "1.0", config -> config.set("enumExtensions", "META-INF/enumextensions.json"))) + .addTextFile("META-INF/enumextensions.json", """ + { + "entries": [ + { + "enum": "enumtestmod/EnumWithId", + "name": "ENUMTESTMOD_PREFIXED_ID_TEST_THING", + "constructor": "(ILjava/lang/String;)V", + "parameters": [ -1, "enumtestmod:prefixed_id_test_thing" ] + }, + { + "enum": "enumtestmod/EnumWithId", + "name": "ENUMTESTMOD_MTH_LAZY_ID_TEST_THING", + "constructor": "(ILjava/lang/String;)V", + "parameters": { + "class": "enumtestmod/TestMod", + "method": "getIdEnumParameter" + } + } + ] + } + """) + .addClass("enumtestmod.EnumWithId", """ + package enumtestmod; + @net.neoforged.fml.common.asm.enumextension.IndexedEnum + @net.neoforged.fml.common.asm.enumextension.NamedEnum(1) + public enum EnumWithId implements net.neoforged.fml.common.asm.enumextension.IExtensibleEnum { + TEST_ID_THING(0, "test"); + private final int id; + private final String name; + EnumWithId(int id, String name) { + this.id = id; + this.name = name; + } + public int getId() { + return id; + } + public static net.neoforged.fml.common.asm.enumextension.ExtensionInfo getExtensionInfo() { + return net.neoforged.fml.common.asm.enumextension.ExtensionInfo.nonExtended(EnumWithId.class); + } + } + """) + .addClass("enumtestmod.TestMod", """ + import net.neoforged.fml.common.asm.enumextension.EnumProxy; + public class TestMod { + public static Object getEnumParameter(int idx, Class type) { + if (idx == 0) { + return type.cast("enumtestmod:mth_lazy_test_thing"); + } + throw new IllegalArgumentException("Unexpected param idx: " + idx); + } + public static Object getIdEnumParameter(int idx, Class type) { + return type.cast(switch (idx) { + case 0 -> -1; + case 1 -> "enumtestmod:mth_lazy_id_test_thing"; + default -> throw new IllegalArgumentException("Unexpected param idx: " + idx); + }); + } + } + """) + .build(); + launchAndLoad("forgeclient"); + + var enumWithId = getEnumClass("enumtestmod.EnumWithId"); + assertThat(enumWithId.getEnumConstants()).extracting(Enum::name).containsExactly( + "TEST_ID_THING", + "ENUMTESTMOD_MTH_LAZY_ID_TEST_THING", + "ENUMTESTMOD_PREFIXED_ID_TEST_THING"); + assertThat(enumWithId.getEnumConstants()).extracting(Enum::ordinal).containsExactly( + 0, + 1, + 2); + var getIdMethod = enumWithId.getMethod("getId"); + assertThat(enumWithId.getEnumConstants()).extracting(getIdMethod::invoke).containsExactly( + 0, + 1, + 2); + } + + private ExtensionInfo getExtensionInfo(Class enumClass) throws Exception { + var getExtensionInfo = enumClass.getMethod("getExtensionInfo"); + return (ExtensionInfo) getExtensionInfo.invoke(null); + } + + @SuppressWarnings("unchecked") + private > Class getEnumClass(String name) throws ClassNotFoundException { + return (Class) Class.forName(name, true, gameClassLoader); + } + + private static Consumer getModsTomlBuilderConsumer(String extensionPath) { + return builder -> { + builder.unlicensedJavaMod(); + builder.addMod("testmod", "1.0", config -> { + config.set("enumExtensions", extensionPath); + }); + }; + } +} diff --git a/loader/src/test/java/net/neoforged/fml/loading/CoreModTest.java b/loader/src/test/java/net/neoforged/fml/loading/CoreModTest.java index 3724c15d1..bf4945ae6 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/CoreModTest.java +++ b/loader/src/test/java/net/neoforged/fml/loading/CoreModTest.java @@ -7,10 +7,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.mock; import cpw.mods.modlauncher.api.ITransformer; +import cpw.mods.modlauncher.api.ITransformerVotingContext; import cpw.mods.modlauncher.api.TargetType; +import cpw.mods.modlauncher.api.TransformerVoteResult; +import java.util.Set; import net.neoforged.fml.ModLoadingException; import net.neoforged.jarjar.metadata.ContainedJarIdentifier; import net.neoforged.neoforgespi.coremod.ICoreMod; @@ -24,7 +26,29 @@ public class CoreModTest extends LauncherTest { private static final ContainedJarIdentifier JAR_IDENTIFIER = new ContainedJarIdentifier("testmod", "coremod"); - public static final ITransformer TEST_TRANSFORMER = mock(ITransformer.class); + // A transformer that just adds a @Deprecated annotation, which is easy to assert for + public static final ITransformer TEST_TRANSFORMER = new ITransformer<>() { + @Override + public ClassNode transform(ClassNode classNode, ITransformerVotingContext context) { + classNode.visitAnnotation("Ljava/lang/Deprecated;", true); + return classNode; + } + + @Override + public TransformerVoteResult castVote(ITransformerVotingContext context) { + return TransformerVoteResult.YES; + } + + @Override + public Set> targets() { + return Set.of(Target.targetClass("testmod.TestClass")); + } + + @Override + public TargetType getTargetType() { + return TargetType.CLASS; + } + }; @Test public void testBrokenJijJavaCoremod() throws Exception { @@ -75,6 +99,9 @@ public void testJavaCoreMod() throws Exception { installation.buildModJar("testmod.jar") .withTestmodModsToml() + .addClass("testmod.TestClass", """ + class TestClass {} + """) .withJarInJar(JAR_IDENTIFIER, coreMod -> { coreMod.withModTypeManifest(IModFile.Type.LIBRARY) .addService(ICoreMod.class.getName(), "testmod.coremods.TestCoreMod") @@ -90,6 +117,9 @@ public class TestCoreMod implements net.neoforged.neoforgespi.coremod.ICoreMod { var transformers = launchAndLoad("forgeclient").transformers(); assertThat(transformers).containsOnly(TEST_TRANSFORMER); + + var testClass = Class.forName("testmod.TestClass", true, gameClassLoader); + assertThat(testClass).hasAnnotation(Deprecated.class); // This is added by the transformer } @SuppressWarnings("unchecked") @@ -104,6 +134,7 @@ public void testJavaScriptCoremod() throws Exception { "coremodid": "coremods/test.js" } """) + .addClass("net.minecraft.world.level.biome.Biome", "class Biome {}") .addTextFile("coremods/test.js", """ function initializeCoreMod() { return { @@ -113,6 +144,7 @@ function initializeCoreMod() { 'name': 'net.minecraft.world.level.biome.Biome' }, 'transformer': function(classNode) { + classNode.visitAnnotation("Ljava/lang/Deprecated;", true); return classNode; } } @@ -127,5 +159,8 @@ function initializeCoreMod() { assertThat(transformer.getTargetType()).isEqualTo(TargetType.CLASS); assertThat(transformer.targets()).containsOnly( ITransformer.Target.targetClass("net.minecraft.world.level.biome.Biome")); + + var testClass = Class.forName("net.minecraft.world.level.biome.Biome", true, gameClassLoader); + assertThat(testClass).hasAnnotation(Deprecated.class); // This is added by the transformer } } diff --git a/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java b/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java index e828570a3..c0543f23b 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java +++ b/loader/src/test/java/net/neoforged/fml/loading/LauncherTest.java @@ -12,18 +12,23 @@ import cpw.mods.cl.JarModuleFinder; import cpw.mods.cl.ModuleClassLoader; import cpw.mods.jarhandling.SecureJar; +import cpw.mods.modlauncher.Environment; +import cpw.mods.modlauncher.LaunchPluginHandler; import cpw.mods.modlauncher.Launcher; +import cpw.mods.modlauncher.TransformStore; +import cpw.mods.modlauncher.TransformationServiceDecorator; +import cpw.mods.modlauncher.TransformingClassLoader; import cpw.mods.modlauncher.api.IEnvironment; import cpw.mods.modlauncher.api.IModuleLayerManager; import cpw.mods.modlauncher.api.ITransformationService; import cpw.mods.modlauncher.api.ITransformer; -import java.io.IOException; import java.lang.module.Configuration; import java.lang.module.ModuleFinder; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -67,10 +72,16 @@ public abstract class LauncherTest { // and ModDirTransformerDiscoverer, which pick up files like mixin. Set locatedPaths = new HashSet<>(); + protected TransformingClassLoader gameClassLoader; + @BeforeEach - void setUp() throws IOException { + void setUp() throws Exception { Launcher.INSTANCE = launcher; - when(Launcher.INSTANCE.findLayerManager()).thenReturn(Optional.of(moduleLayerManager)); + when(launcher.findLayerManager()).thenReturn(Optional.of(moduleLayerManager)); + var environmentCtor = Environment.class.getDeclaredConstructor(Launcher.class); + environmentCtor.setAccessible(true); + var environment = environmentCtor.newInstance(launcher); + when(launcher.environment()).thenReturn(environment); installation = new SimulatedInstallation(); @@ -81,6 +92,7 @@ void setUp() throws IOException { @AfterEach void clearSystemProperties() throws Exception { + gameClassLoader = null; installation.close(); Launcher.INSTANCE = null; } @@ -202,7 +214,7 @@ public List values(OptionSpec options) { (List>) transformers); } - private void loadMods(LaunchResult launchResult) { + private void loadMods(LaunchResult launchResult) throws Exception { FMLLoader.progressWindowTick = () -> {}; // build the game layer @@ -210,11 +222,28 @@ private void loadMods(LaunchResult launchResult) { var parentConfigs = parents.stream().map(ModuleLayer::configuration).toList(); var gameLayerFinder = JarModuleFinder.of(launchResult.gameLayerModules().values().toArray(new SecureJar[0])); var configuration = Configuration.resolveAndBind(ModuleFinder.of(), parentConfigs, gameLayerFinder, launchResult.gameLayerModules().keySet()); - var classLoader = new ModuleClassLoader("GAME", configuration, parents, getClass().getClassLoader()); + /* + * Does the minimum to get a transforming classloader. + */ + var transformStore = new TransformStore(); + new TransformationServiceDecorator(serviceProvider).gatherTransformers(transformStore); + + Launcher.INSTANCE.environment().computePropertyIfAbsent(IEnvironment.Keys.MODLIST.get(), ignored1 -> new ArrayList<>()); + var lph = new LaunchPluginHandler(environment.getLaunchPlugins()); + gameClassLoader = new TransformingClassLoader( + transformStore, + lph, + launcher.environment(), + configuration, + parents, + getClass().getClassLoader()); + var controller = ModuleLayer.defineModules( configuration, parents, - ignored -> classLoader); + ignored -> gameClassLoader); + moduleLayerManager.setLayer(IModuleLayerManager.Layer.BOOT, ModuleLayer.empty()); + moduleLayerManager.setLayer(IModuleLayerManager.Layer.GAME, controller.layer()); FMLLoader.beforeStart(controller.layer()); diff --git a/loader/src/test/java/net/neoforged/fml/loading/ModFileBuilder.java b/loader/src/test/java/net/neoforged/fml/loading/ModFileBuilder.java index e0409641e..3564e30e5 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/ModFileBuilder.java +++ b/loader/src/test/java/net/neoforged/fml/loading/ModFileBuilder.java @@ -13,15 +13,12 @@ import java.nio.file.FileSystem; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import net.neoforged.fml.test.RuntimeCompiler; @@ -32,10 +29,6 @@ import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.artifact.versioning.VersionRange; import org.intellij.lang.annotations.Language; -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.ClassNode; public class ModFileBuilder { public static final ContainedVersion JIJ_V1 = new ContainedVersion(VersionRange.createFromVersion("1.0"), new DefaultArtifactVersion("1.0")); @@ -48,7 +41,6 @@ public class ModFileBuilder { private final List content = new ArrayList<>(); private final Manifest manifest = new Manifest(); private final List jijEntries = new ArrayList<>(); - private final List> transforms = new ArrayList<>(); // Info that will end up in the mods.toml @@ -152,14 +144,6 @@ public ModFileBuilder withJarInJar(ContainedJarIdentifier identifier, ContainedV return this; } - /** - * Statically applies the given class-node transforms to each class in the jar-file. - */ - public ModFileBuilder withTransform(BiFunction transform) { - this.transforms.add(transform); - return this; - } - public Path build() throws IOException { compilationBuilder.compile(); @@ -201,60 +185,11 @@ public Path build() throws IOException { } } - if (!transforms.isEmpty()) { - transformJar(destination, transforms); - } - close(); return destination; } - private static void transformJar(Path destination, - List> transforms) throws IOException { - var transformedJar = destination.resolveSibling(destination.getFileName() + ".transformed"); - - // In the absence of a transforming class-loader, pre-transform everything - try (var in = new JarInputStream(Files.newInputStream(destination))) { - try (var out = new JarOutputStream(Files.newOutputStream(transformedJar), in.getManifest())) { - for (var entry = in.getNextEntry(); entry != null; entry = in.getNextEntry()) { - var path = entry.getName(); - - out.putNextEntry(new JarEntry(path)); - if (path.endsWith(".class")) { - var classData = in.readAllBytes(); - var classNode = new ClassNode(); - ClassReader classReader = new ClassReader(classData); - classReader.accept(classNode, ClassReader.EXPAND_FRAMES); - - // TRANSFORM CLASS - var classType = getTypeFromPath(path); - for (var transform : transforms) { - classNode = transform.apply(classType, classNode); - } - - var cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); - classNode.accept(cw); - out.write(cw.toByteArray()); - } else { - in.transferTo(out); - } - out.closeEntry(); - } - } - } - - // Move it over - Files.move(transformedJar, destination, StandardCopyOption.REPLACE_EXISTING); - } - - private static Type getTypeFromPath(String entry) { - // Remove .class suffix - var className = entry.substring(0, entry.length() - ".class".length()); - - return Type.getType("L" + className + ";"); - } - public void close() throws IOException { compiler.close(); } diff --git a/loader/src/test/java/net/neoforged/fml/loading/TestEnvironment.java b/loader/src/test/java/net/neoforged/fml/loading/TestEnvironment.java index 1643d95f8..7492c4ac5 100644 --- a/loader/src/test/java/net/neoforged/fml/loading/TestEnvironment.java +++ b/loader/src/test/java/net/neoforged/fml/loading/TestEnvironment.java @@ -10,11 +10,14 @@ import cpw.mods.modlauncher.api.IModuleLayerManager; import cpw.mods.modlauncher.api.TypesafeMap; import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import java.util.Objects; import java.util.Optional; import java.util.ServiceLoader; import java.util.function.Function; +import java.util.stream.Stream; import net.neoforged.accesstransformer.ml.AccessTransformerService; import net.neoforged.fml.common.asm.RuntimeDistCleaner; +import net.neoforged.fml.common.asm.enumextension.RuntimeEnumExtender; import org.jetbrains.annotations.Nullable; public class TestEnvironment implements IEnvironment { @@ -24,6 +27,8 @@ public class TestEnvironment implements IEnvironment { public AccessTransformerService accessTransformerService = new AccessTransformerService(); @Nullable public RuntimeDistCleaner runtimeDistCleaner = new RuntimeDistCleaner(); + @Nullable + public RuntimeEnumExtender runtimeEnumExtender = new RuntimeEnumExtender(); public TestEnvironment(TestModuleLayerManager moduleLayerManager) { this.moduleLayerManager = moduleLayerManager; @@ -41,11 +46,7 @@ public T computePropertyIfAbsent(TypesafeMap.Key key, Function findLaunchPlugin(String name) { - return switch (name) { - case "accesstransformer" -> Optional.ofNullable(accessTransformerService); - case "runtimedistcleaner" -> Optional.ofNullable(runtimeDistCleaner); - default -> throw new IllegalStateException("Unexpected value: " + name); - }; + return getLaunchPlugins().filter(lp -> lp.name().equals(name)).findFirst(); } @Override @@ -60,4 +61,10 @@ public Optional findLaunchHandler(String name) { public Optional findModuleLayerManager() { return Optional.of(moduleLayerManager); } + + public Stream getLaunchPlugins() { + return Stream.of(accessTransformerService, + runtimeDistCleaner, + runtimeEnumExtender).filter(Objects::nonNull); + } }