diff --git a/README.md b/README.md index 6a5b61b..dd19bf2 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,23 @@ # CoreMods -New JavaScript based system for implementing CoreMods. +CoreMods is a JavaScript-based system that acts as a wrapper around ObjectWeb ASM. -Why? +## Purpose -Because it means that it's a lot easier to manage the lifecycle correctly. We can isolate -CoreMod logic to the proper ClassLoading contexts without effort on the part of the Modder. +CoreMods need to be sandboxed, or otherwise isolated, in their own environments so that they are not able to cause early +class-loading. They transform classes as only as they are loaded and do not have access to objects outside of the +sandbox given to them. This helps prevent issues that would otherwise arise from CoreMods written traditionally in Java. -It hopefully also communicates that CoreMods are strictly arms-length : they operate on -classes as they load _only_ - changing structures and behaviours through that means. +Since CoreMods integrates with ModLauncher's transformation system, it is easier to manage the lifecycle as CoreMods is +only responsible for managing the transformation as ModLauncher is instead the one responsible for providing the class +loading system. -This is connected to Forge and FML through the CoreMod SPI being implemented in new Forge. \ No newline at end of file +## Usage + +CoreMods are JavaScript files that are sandboxed by the limitations provided within the CoreMod engine. It is only able +to access a limited set of classes and packages. ASMAPI, included within CoreMods, exists to provide several helpful +tools for writing CoreMods. You can view this class yourself to see its usages, or you can find examples of it in other +CoreMods. + +The best way to find examples for CoreMods is to look at Forge itself, since it includes complex examples that utilize +much of the functionality within the sandbox. diff --git a/build.gradle b/build.gradle index d39db03..aef9dce 100644 --- a/build.gradle +++ b/build.gradle @@ -39,11 +39,15 @@ dependencies { testImplementation('org.powermock:powermock-core:2.0.+') testImplementation('org.hamcrest:hamcrest-core:2.2') testImplementation('org.apache.logging.log4j:log4j-core:2.19.0') + testImplementation(libs.modlauncher) + testImplementation(libs.forgespi) + testImplementation(libs.unsafe) testCompileOnly('org.jetbrains:annotations:21.0.1') testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.7.+') testRuntimeOnly(project(':coremods-test-jar')) - + compileOnly(libs.modlauncher) + compileOnly(libs.securemodules) implementation(libs.log4j.api) api(libs.bundles.asm) compileOnly(libs.forgespi) diff --git a/coremods-test/build.gradle b/coremods-test/build.gradle index dad62c5..bb2288f 100644 --- a/coremods-test/build.gradle +++ b/coremods-test/build.gradle @@ -41,6 +41,9 @@ dependencies { testImplementation(project(':coremods-test-jar')) testImplementation(libs.junit.api) testImplementation(libs.log4j.api) + testImplementation(libs.modlauncher) + testImplementation(libs.forgespi) + testImplementation(libs.unsafe) testRuntimeOnly(libs.bundles.junit.runtime) testCompileOnly(libs.nulls) } diff --git a/coremods-test/src/test/java/module-info.java b/coremods-test/src/test/java/module-info.java index 7ad6cd4..558b200 100644 --- a/coremods-test/src/test/java/module-info.java +++ b/coremods-test/src/test/java/module-info.java @@ -3,10 +3,15 @@ * SPDX-License-Identifier: LGPL-2.1-only */ module net.minecraftforge.coremods.test { + requires cpw.mods.modlauncher; requires net.minecraftforge.forgespi; - requires org.junit.jupiter.api; - requires org.apache.logging.log4j; - requires cpw.mods.modlauncher; - requires org.objectweb.asm.tree; - requires org.jetbrains.annotations; -} \ No newline at end of file + requires net.minecraftforge.coremod; + requires org.junit.jupiter.api; + requires org.apache.logging.log4j; + requires org.objectweb.asm.tree; + requires org.jetbrains.annotations; + requires net.minecraftforge.unsafe; + + provides cpw.mods.modlauncher.api.ITransformationService + with net.minecraftforge.coremod.test.TestTransformerService; +} diff --git a/coremods-test/src/test/java/net/minecraftforge/coremod/test/CoreModTest.java b/coremods-test/src/test/java/net/minecraftforge/coremod/test/CoreModTest.java index fa781d2..ee3b420 100644 --- a/coremods-test/src/test/java/net/minecraftforge/coremod/test/CoreModTest.java +++ b/coremods-test/src/test/java/net/minecraftforge/coremod/test/CoreModTest.java @@ -6,9 +6,12 @@ import net.minecraftforge.coremod.CoreModEngine; +import java.lang.reflect.InvocationTargetException; import java.util.List; import java.util.stream.Collectors; +import net.minecraftforge.forgespi.coremod.ICoreModFile; +import net.minecraftforge.unsafe.UnsafeHacks; import org.junit.jupiter.api.Test; import org.objectweb.asm.tree.ClassNode; @@ -18,11 +21,13 @@ public class CoreModTest { @SuppressWarnings("unchecked") @Test - void testJSLoading() { + void testJSLoading() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { final CoreModEngine coreModEngine = new CoreModEngine(); - coreModEngine.loadCoreMod(new JSFileLoader("src/test/javascript/testcoremod.js")); - coreModEngine.loadCoreMod(new JSFileLoader("src/test/javascript/testcore2mod.js")); - coreModEngine.loadCoreMod(new JSFileLoader("src/test/javascript/testdata.js")); + var loadCoreMod = coreModEngine.getClass().getMethod("loadCoreMod", ICoreModFile.class); + UnsafeHacks.setAccessible(loadCoreMod); + loadCoreMod.invoke(coreModEngine, new JSFileLoader("src/test/javascript/testcoremod.js")); + loadCoreMod.invoke(coreModEngine, new JSFileLoader("src/test/javascript/testcore2mod.js")); + loadCoreMod.invoke(coreModEngine, new JSFileLoader("src/test/javascript/testdata.js")); final List> iTransformers = coreModEngine.initializeCoreMods(); iTransformers.forEach(t -> { System.out.printf("targ: %s\n", t.targets().stream().map(ITransformer.Target::getClassName).collect(Collectors.joining(","))); diff --git a/coremods-test/src/test/java/net/minecraftforge/coremod/test/TestLaunchTransformer.java b/coremods-test/src/test/java/net/minecraftforge/coremod/test/TestLaunchTransformer.java index f68f123..5b42445 100644 --- a/coremods-test/src/test/java/net/minecraftforge/coremod/test/TestLaunchTransformer.java +++ b/coremods-test/src/test/java/net/minecraftforge/coremod/test/TestLaunchTransformer.java @@ -9,8 +9,11 @@ import cpw.mods.modlauncher.api.ITransformer; import net.minecraftforge.coremod.CoreModEngine; +import net.minecraftforge.forgespi.coremod.ICoreModFile; +import net.minecraftforge.unsafe.UnsafeHacks; import org.junit.jupiter.api.Test; +import java.lang.reflect.InvocationTargetException; import java.util.List; import java.util.concurrent.Callable; @@ -21,14 +24,16 @@ public static List> getTransformers() { } @Test - public void testCoreModLoading() { + public void testCoreModLoading() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { System.setProperty("test.harness", "out/production/classes,out/test/classes,out/testJars/classes,build/classes/java/main,build/classes/java/test,build/classes/java/testJars"); System.setProperty("test.harness.callable", "net.minecraftforge.coremod.test.TestLaunchTransformer$Callback"); - cme.loadCoreMod(new JSFileLoader("src/test/javascript/testcoremod.js")); - cme.loadCoreMod(new JSFileLoader("src/test/javascript/testcore2mod.js")); - cme.loadCoreMod(new JSFileLoader("src/test/javascript/testmethodcoremod.js")); - cme.loadCoreMod(new JSFileLoader("src/test/javascript/testmethodcoreinsert.js")); + var loadCoreMod = cme.getClass().getMethod("loadCoreMod", ICoreModFile.class); + UnsafeHacks.setAccessible(loadCoreMod); + loadCoreMod.invoke(cme, new JSFileLoader("src/test/javascript/testcoremod.js")); + loadCoreMod.invoke(cme, new JSFileLoader("src/test/javascript/testcore2mod.js")); + loadCoreMod.invoke(cme, new JSFileLoader("src/test/javascript/testmethodcoremod.js")); + loadCoreMod.invoke(cme, new JSFileLoader("src/test/javascript/testmethodcoreinsert.js")); Launcher.main("--version", "1.0", "--launchTarget", "testharness"); } diff --git a/coremods-test/src/test/javascript/testcore2mod.js b/coremods-test/src/test/javascript/testcore2mod.js index 849d67c..c4c2074 100644 --- a/coremods-test/src/test/javascript/testcore2mod.js +++ b/coremods-test/src/test/javascript/testcore2mod.js @@ -1,3 +1,7 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ var core2fn = function() { print("Core2 function!"); } diff --git a/coremods-test/src/test/javascript/testcoremod.js b/coremods-test/src/test/javascript/testcoremod.js index c7a6f08..2607cbb 100644 --- a/coremods-test/src/test/javascript/testcoremod.js +++ b/coremods-test/src/test/javascript/testcoremod.js @@ -1,3 +1,7 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ function initializeCoreMod() { print("Hello"); Java.type('net.minecraftforge.coremod.api.ASMAPI').loadFile('testcoremod2.js') diff --git a/coremods-test/src/test/javascript/testcoremod2.js b/coremods-test/src/test/javascript/testcoremod2.js index 9b49479..55edd91 100644 --- a/coremods-test/src/test/javascript/testcoremod2.js +++ b/coremods-test/src/test/javascript/testcoremod2.js @@ -1,3 +1,7 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ function moreFunctions() { print("Poopy from more functions!"); } \ No newline at end of file diff --git a/coremods-test/src/test/javascript/testdata.js b/coremods-test/src/test/javascript/testdata.js index a9557e5..2c991ab 100644 --- a/coremods-test/src/test/javascript/testdata.js +++ b/coremods-test/src/test/javascript/testdata.js @@ -1,3 +1,7 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ function initializeCoreMod() { var data = Java.type('net.minecraftforge.coremod.api.ASMAPI').loadData('testdata.json') print('Loaded JSON: ' + JSON.stringify(data)) diff --git a/coremods-test/src/test/javascript/testmethodcoreinsert.js b/coremods-test/src/test/javascript/testmethodcoreinsert.js index ed9d69c..3639cb1 100644 --- a/coremods-test/src/test/javascript/testmethodcoreinsert.js +++ b/coremods-test/src/test/javascript/testmethodcoreinsert.js @@ -1,9 +1,13 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ function initializeCoreMod() { return { 'coremodmethod': { 'target': { 'type': 'METHOD', - 'class': 'cpw.mods.TestClass', + 'class': 'net.minecraftforge.coremods.testjar.TestClass', 'methodName': 'testInsert', 'methodDesc': '()Z' }, diff --git a/coremods-test/src/test/javascript/testmethodcoremod.js b/coremods-test/src/test/javascript/testmethodcoremod.js index 98c58a7..5b9ead2 100644 --- a/coremods-test/src/test/javascript/testmethodcoremod.js +++ b/coremods-test/src/test/javascript/testmethodcoremod.js @@ -1,9 +1,13 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ function initializeCoreMod() { return { 'coremodmethod': { 'target': { 'type': 'METHOD', - 'class': 'cpw.mods.TestClass', + 'class': 'net.minecraftforge.coremods.testjar.TestClass', 'methodName': 'testMethod', 'methodDesc': '()Z' }, diff --git a/coremods-test/src/test/resources/META-INF/services/cpw.mods.modlauncher.api.ITransformationService b/coremods-test/src/test/resources/META-INF/services/cpw.mods.modlauncher.api.ITransformationService deleted file mode 100644 index 2e1bdef..0000000 --- a/coremods-test/src/test/resources/META-INF/services/cpw.mods.modlauncher.api.ITransformationService +++ /dev/null @@ -1 +0,0 @@ -net.minecraftforge.coremod.test.TestTransformerService \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 6e58746..33b2119 100644 --- a/settings.gradle +++ b/settings.gradle @@ -24,9 +24,9 @@ dependencyResolutionManagement { library('junit-platform-launcher', 'org.junit.platform:junit-platform-launcher:1.10.0') bundle('junit-runtime', ['junit-engine', 'junit-platform-launcher']) + library('unsafe', 'net.minecraftforge:unsafe:0.9.2') + library('securemodules', 'net.minecraftforge:securemodules:2.2.20') // Needs unsafe /* - library('unsafe', 'net.minecraftforge:unsafe:0.9.1') - library('securemodules', 'net.minecraftforge:securemodules:2.2.0') // Needs unsafe library('gson', 'com.google.code.gson:gson:2.10.1') library('jopt-simple', 'net.sf.jopt-simple:jopt-simple:5.0.4') */ @@ -34,7 +34,7 @@ dependencyResolutionManagement { library('forgespi', 'net.minecraftforge:forgespi:7.1.0') library('modlauncher', 'net.minecraftforge:modlauncher:10.1.1') // Needs securemodules library('nulls', 'org.jetbrains:annotations:23.0.0') - library('nashorn', 'org.openjdk.nashorn:nashorn-core:15.3') // Needed by coremods, because the JRE no longer ships JS + library('nashorn', 'org.openjdk.nashorn:nashorn-core:15.4') // Needed by coremods, because the JRE no longer ships JS version('log4j', '2.19.0') library('log4j-api', 'org.apache.logging.log4j', 'log4j-api' ).versionRef('log4j') diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..1d81b88 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +module net.minecraftforge.coremod { + // CoreMods framework + exports net.minecraftforge.coremod; + // ASMAPI + exports net.minecraftforge.coremod.api; + + requires cpw.mods.modlauncher; + requires net.minecraftforge.forgespi; + requires org.apache.logging.log4j; + requires org.jetbrains.annotations; + requires org.openjdk.nashorn; + requires org.objectweb.asm.util; + + provides net.minecraftforge.forgespi.coremod.ICoreModProvider + with net.minecraftforge.coremod.CoreModProvider; +} diff --git a/src/main/java/net/minecraftforge/coremod/CoreMod.java b/src/main/java/net/minecraftforge/coremod/CoreMod.java index c7f632f..8ff0a08 100644 --- a/src/main/java/net/minecraftforge/coremod/CoreMod.java +++ b/src/main/java/net/minecraftforge/coremod/CoreMod.java @@ -4,24 +4,45 @@ */ package net.minecraftforge.coremod; -import cpw.mods.modlauncher.api.*; +import cpw.mods.modlauncher.api.ITransformer; import net.minecraftforge.coremod.api.ASMAPI; import net.minecraftforge.coremod.transformer.CoreModClassTransformer; import net.minecraftforge.coremod.transformer.CoreModFieldTransformer; import net.minecraftforge.coremod.transformer.CoreModMethodTransformer; -import net.minecraftforge.forgespi.coremod.*; -import org.apache.logging.log4j.*; +import net.minecraftforge.forgespi.coremod.ICoreModFile; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; import org.jetbrains.annotations.Nullable; -import javax.script.*; -import java.io.*; -import java.nio.file.*; -import java.util.*; +import javax.script.Bindings; +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptException; +import java.io.IOException; +import java.io.Reader; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.Function; -import java.util.stream.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +/** + * Represents a coremod. + */ public class CoreMod { + /** + * Used to log events inside coremods. + * + * @see ASMAPI#log(String, String, Object...) + */ public static final Marker COREMODLOG = MarkerManager.getMarker("COREMODLOG").addParents(MarkerManager.getMarker("COREMOD")); + private final ICoreModFile file; private final ScriptEngine scriptEngine; private Map javaScript; @@ -34,17 +55,23 @@ public class CoreMod { this.scriptEngine = scriptEngine; } + /** + * Gets the path to the coremod file. + * + * @return The path + */ public Path getPath() { return this.file.getPath(); } @SuppressWarnings("unchecked") void initialize() { - logger = LogManager.getLogger("net.minecraftforge.coremod.CoreMod."+this.file.getOwnerId()); + this.logger = LogManager.getLogger("net.minecraftforge.coremod.CoreMod." + this.file.getOwnerId()); try { - scriptEngine.eval(file.readCoreMod()); + // TODO would it be better to setCoreMod before eval? this would allow loadFile and loadData to be run in the global context + this.scriptEngine.eval(this.file.readCoreMod()); CoreModTracker.setCoreMod(this); - this.javaScript = (Map) ((Invocable) scriptEngine).invokeFunction("initializeCoreMod"); + this.javaScript = (Map) ((Invocable) this.scriptEngine).invokeFunction("initializeCoreMod"); CoreModTracker.clearCoreMod(); this.loaded = true; } catch (IOException | ScriptException | NoSuchMethodException e) { @@ -60,57 +87,97 @@ List> buildTransformers() { } @SuppressWarnings("unchecked") - private ITransformer buildCore(Map.Entry entry) { + private ITransformer buildCore(Map.Entry entry) { final String coreName = entry.getKey(); final Bindings data = entry.getValue(); - final Map targetData = (Map)data.get("target"); - final ITransformer.TargetType targetType = ITransformer.TargetType.valueOf((String)targetData.get("type")); + final Map targetData = (Map) data.get("target"); + final ITransformer.TargetType targetType = ITransformer.TargetType.valueOf((String) targetData.get("type")); final Set targets; - final Bindings function = (Bindings)data.get("transformer"); + final Bindings function = (Bindings) data.get("transformer"); switch (targetType) { case CLASS: if (targetData.containsKey("names")) { - Function, Map> names = NashornFactory.getFunction((Bindings)targetData.get("names")); - targets = names.apply(targetData).values().stream().map(o -> (String)o).map(ITransformer.Target::targetClass).collect(Collectors.toSet()); + Function, Map> names = NashornFactory.getFunction((Bindings) targetData.get("names")); + targets = names.apply(targetData).values().stream().map(o -> (String) o).map(ITransformer.Target::targetClass).collect(Collectors.toSet()); } else - targets = Stream.of(ITransformer.Target.targetClass((String)targetData.get("name"))).collect(Collectors.toSet()); + targets = Stream.of(ITransformer.Target.targetClass((String) targetData.get("name"))).collect(Collectors.toSet()); return new CoreModClassTransformer(this, coreName, targets, NashornFactory.getFunction(function)); case METHOD: targets = Collections.singleton(ITransformer.Target.targetMethod( - (String) targetData.get("class"), ASMAPI.mapMethod((String) targetData.get("methodName")), (String) targetData.get("methodDesc"))); + (String) targetData.get("class"), ASMAPI.mapMethod((String) targetData.get("methodName")), (String) targetData.get("methodDesc"))); return new CoreModMethodTransformer(this, coreName, targets, NashornFactory.getFunction(function)); case FIELD: targets = Collections.singleton(ITransformer.Target.targetField( - (String) targetData.get("class"), ASMAPI.mapField((String) targetData.get("fieldName")))); + (String) targetData.get("class"), ASMAPI.mapField((String) targetData.get("fieldName")))); return new CoreModFieldTransformer(this, coreName, targets, NashornFactory.getFunction(function)); default: throw new RuntimeException("Unimplemented target type " + targetData); } } + /** + * Returns whether the coremod has an error. Usually paired with {@link #getError()}. + * + * @return {@code true} if the coremod has an error + */ public boolean hasError() { - return !loaded; + return !this.loaded; } + /** + * Returns the error that occurred while loading the coremod. Only consider valid if {@link #hasError()} returns + * {@code true}. + * + * @return The error that occurred + */ public Exception getError() { - return error; + return this.error; } + /** + * Gets the coremod file. + * + * @return The coremod file + */ public ICoreModFile getFile() { - return file; + return this.file; } + /** + * Loads an additional file into the script engine. + * + * @param fileName The file to load + * @return {@code true} if loading was successful + * + * @throws ScriptException If the script engine encounters an error, usually due to a syntax error in the script + * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file + */ public boolean loadAdditionalFile(final String fileName) throws ScriptException, IOException { - if (loaded) return false; - Reader additional = file.getAdditionalFile(fileName); - scriptEngine.eval(additional); + // why does this method return a boolean if we're going to crash anyways on load failure? + // it looks like the case of the coremod not being tracked is never reached + if (this.loaded) return false; + + Reader additional = this.file.getAdditionalFile(fileName); + this.scriptEngine.eval(additional); return true; } + /** + * Loads additional JSON data from a file into an {@link Object}. + * + * @param fileName The file to load + * @return The loaded JSON data if successful, or {@code null} if not + * + * @throws ScriptException If the parsed JSON data is malformed + * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file + */ @Nullable public Object loadAdditionalData(final String fileName) throws ScriptException, IOException { - if (loaded) return null; - Reader additional = file.getAdditionalFile(fileName); + // again with this shit, dude! why are we going to return null?? + // isn't the coremod always going to be tracked if it calls ASMAPI.loadData from itself? + if (this.loaded) return null; + + Reader additional = this.file.getAdditionalFile(fileName); char[] buf = new char[4096]; StringBuilder builder = new StringBuilder(); @@ -119,10 +186,18 @@ public Object loadAdditionalData(final String fileName) throws ScriptException, builder.append(buf, 0, numChars); String str = builder.toString(); - return scriptEngine.eval("tmp_json_loading_variable = " + str + ";"); + return this.scriptEngine.eval("tmp_json_loading_variable = " + str + ";"); } + /** + * Logs a message from the coremod. + * + * @param level The log level + * @param message The message + * @param args Any formatting arguments + * @see ASMAPI#log(String, String, Object...) + */ public void logMessage(final String level, final String message, final Object[] args) { - logger.log(Level.getLevel(level), COREMODLOG, message, args); + this.logger.log(Level.getLevel(level), COREMODLOG, message, args); } } diff --git a/src/main/java/net/minecraftforge/coremod/CoreModEngine.java b/src/main/java/net/minecraftforge/coremod/CoreModEngine.java index 5920114..710ab5c 100644 --- a/src/main/java/net/minecraftforge/coremod/CoreModEngine.java +++ b/src/main/java/net/minecraftforge/coremod/CoreModEngine.java @@ -5,56 +5,73 @@ package net.minecraftforge.coremod; import cpw.mods.modlauncher.Launcher; -import cpw.mods.modlauncher.api.*; -import net.minecraftforge.forgespi.coremod.*; +import cpw.mods.modlauncher.api.ITransformer; +import cpw.mods.modlauncher.api.TypesafeMap; +import net.minecraftforge.forgespi.coremod.ICoreModFile; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; -import javax.script.*; -import java.util.*; -import java.util.stream.*; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +/** + * The global coremod engine, responsible for loading and initializing coremods. + */ public class CoreModEngine { private static final Logger LOGGER = LogManager.getLogger(); private static final Marker COREMOD = MarkerManager.getMarker("COREMOD"); private List coreMods = new ArrayList<>(); - static final Set ALLOWED_PACKAGES = new HashSet<>(Arrays.asList( - "java.util", - "java.util.function", - "org.objectweb.asm.util" // ASM util has nice debugging things like Trace visitors - )); - static final Set ALLOWED_CLASSES = new HashSet<>(Arrays.asList( - "net.minecraftforge.coremod.api.ASMAPI","org.objectweb.asm.Opcodes", - // Editing the code of methods - "org.objectweb.asm.tree.AbstractInsnNode","org.objectweb.asm.tree.FieldInsnNode", - "org.objectweb.asm.tree.FrameNode","org.objectweb.asm.tree.IincInsnNode", - "org.objectweb.asm.tree.InsnNode","org.objectweb.asm.tree.IntInsnNode", - "org.objectweb.asm.tree.InsnList", "org.objectweb.asm.tree.InvokeDynamicInsnNode", - "org.objectweb.asm.tree.JumpInsnNode", "org.objectweb.asm.tree.LabelNode", - "org.objectweb.asm.tree.LdcInsnNode", "org.objectweb.asm.tree.LineNumberNode", - "org.objectweb.asm.tree.LocalVariableAnnotationNode", "org.objectweb.asm.tree.LocalVariableNode", - "org.objectweb.asm.tree.LookupSwitchInsnNode", "org.objectweb.asm.tree.MethodInsnNode", - "org.objectweb.asm.tree.MultiANewArrayInsnNode", "org.objectweb.asm.tree.TableSwitchInsnNode", - "org.objectweb.asm.tree.TryCatchBlockNode", "org.objectweb.asm.tree.TypeAnnotationNode", - "org.objectweb.asm.tree.TypeInsnNode", "org.objectweb.asm.tree.VarInsnNode", + static final Set ALLOWED_PACKAGES = Set.of( + "java.util", + "java.util.function", + "org.objectweb.asm.util" // ASM util has nice debugging things like Trace visitors + ); + + static final Set ALLOWED_CLASSES = Set.of( + "net.minecraftforge.coremod.api.ASMAPI", "org.objectweb.asm.Opcodes", + + // Editing the code of methods + "org.objectweb.asm.tree.AbstractInsnNode", "org.objectweb.asm.tree.FieldInsnNode", + "org.objectweb.asm.tree.FrameNode", "org.objectweb.asm.tree.IincInsnNode", + "org.objectweb.asm.tree.InsnNode", "org.objectweb.asm.tree.IntInsnNode", + "org.objectweb.asm.tree.InsnList", "org.objectweb.asm.tree.InvokeDynamicInsnNode", + "org.objectweb.asm.tree.JumpInsnNode", "org.objectweb.asm.tree.LabelNode", + "org.objectweb.asm.tree.LdcInsnNode", "org.objectweb.asm.tree.LineNumberNode", + "org.objectweb.asm.tree.LocalVariableAnnotationNode", "org.objectweb.asm.tree.LocalVariableNode", + "org.objectweb.asm.tree.LookupSwitchInsnNode", "org.objectweb.asm.tree.MethodInsnNode", + "org.objectweb.asm.tree.MultiANewArrayInsnNode", "org.objectweb.asm.tree.TableSwitchInsnNode", + "org.objectweb.asm.tree.TryCatchBlockNode", "org.objectweb.asm.tree.TypeAnnotationNode", + "org.objectweb.asm.tree.TypeInsnNode", "org.objectweb.asm.tree.VarInsnNode", - // Adding new fields to classes - "org.objectweb.asm.tree.FieldNode", + // Adding new fields to classes + "org.objectweb.asm.tree.FieldNode", - // Adding new methods to classes - "org.objectweb.asm.tree.MethodNode","org.objectweb.asm.tree.ParameterNode", + // Adding new methods to classes + "org.objectweb.asm.tree.MethodNode", "org.objectweb.asm.tree.ParameterNode", - // Misc stuff referenced in above classes that's probably useful - "org.objectweb.asm.Attribute","org.objectweb.asm.Handle", - "org.objectweb.asm.Label","org.objectweb.asm.Type", - "org.objectweb.asm.TypePath","org.objectweb.asm.TypeReference" - )); + // Misc stuff referenced in above classes that's probably useful + "org.objectweb.asm.Attribute", "org.objectweb.asm.Handle", + "org.objectweb.asm.Label", "org.objectweb.asm.Type", + "org.objectweb.asm.TypePath", "org.objectweb.asm.TypeReference" + ); - // this is enabled by FML in Minecraft 1.21.1 and earlier, but disabled in 1.21.3 and later - // see ASMAPI.findFirstInstructionBefore for more details + /** + * Whether to preserve the legacy behavior of + * {@link net.minecraftforge.coremod.api.ASMAPI#findFirstInstructionBefore(org.objectweb.asm.tree.MethodNode, int, + * int)} for backwards-compatibility. + *

+ * In Forge's case, this is set by FML in Minecraft 1.21.1 and earlier, but not in 1.21.3 and later. + * + * @see net.minecraftforge.coremod.api.ASMAPI#findFirstInstructionBefore(org.objectweb.asm.tree.MethodNode, int, + * int) + */ public static final boolean DO_NOT_FIX_INSNBEFORE; static { @@ -75,23 +92,28 @@ void loadCoreMod(ICoreModFile coremod) { jsContext.removeAttribute("quit", jsContext.getAttributesScope("quit")); jsContext.removeAttribute("loadWithNewGlobal", jsContext.getAttributesScope("loadWithNewGlobal")); jsContext.removeAttribute("exit", jsContext.getAttributesScope("exit")); - coreMods.add(new CoreMod(coremod, scriptEngine)); + this.coreMods.add(new CoreMod(coremod, scriptEngine)); } static boolean checkClass(String s) { return ALLOWED_CLASSES.contains(s) || (s.lastIndexOf('.') != -1 && ALLOWED_PACKAGES.contains(s.substring(0, s.lastIndexOf('.')))); } + /** + * Initializes all coremods that were added by any {@link CoreModProvider}s. + * + * @return A list of transformers given by the coremods + */ public List> initializeCoreMods() { - coreMods.forEach(this::initialize); - return coreMods.stream().map(CoreMod::buildTransformers).flatMap(List::stream).collect(Collectors.toList()); + for (CoreMod coreMod : this.coreMods) this.initialize(coreMod); + return this.coreMods.stream().map(CoreMod::buildTransformers).flatMap(List::stream).collect(Collectors.toList()); } private void initialize(final CoreMod coreMod) { - LOGGER.debug(COREMOD,"Loading CoreMod from {}", coreMod.getPath()); + LOGGER.debug(COREMOD, "Loading CoreMod from {}", coreMod.getPath()); coreMod.initialize(); if (coreMod.hasError()) { - LOGGER.error(COREMOD,"Error occurred initializing CoreMod", coreMod.getError()); + LOGGER.error(COREMOD, "Error occurred initializing CoreMod", coreMod.getError()); } else { LOGGER.debug(COREMOD, "CoreMod loaded successfully"); } diff --git a/src/main/java/net/minecraftforge/coremod/CoreModProvider.java b/src/main/java/net/minecraftforge/coremod/CoreModProvider.java index 03db991..f28b06e 100644 --- a/src/main/java/net/minecraftforge/coremod/CoreModProvider.java +++ b/src/main/java/net/minecraftforge/coremod/CoreModProvider.java @@ -4,20 +4,25 @@ */ package net.minecraftforge.coremod; -import cpw.mods.modlauncher.api.*; -import net.minecraftforge.forgespi.coremod.*; +import cpw.mods.modlauncher.api.ITransformer; +import net.minecraftforge.forgespi.coremod.ICoreModFile; +import net.minecraftforge.forgespi.coremod.ICoreModProvider; -import java.util.*; +import java.util.List; +/** + * Exposes the CoreMods system to external systems via ForgeSPI (i.e. FML). + */ public class CoreModProvider implements ICoreModProvider { - private CoreModEngine engine = new CoreModEngine(); + private final CoreModEngine engine = new CoreModEngine(); + @Override public void addCoreMod(final ICoreModFile file) { - engine.loadCoreMod(file); + this.engine.loadCoreMod(file); } @Override public List> getCoreModTransformers() { - return engine.initializeCoreMods(); + return this.engine.initializeCoreMods(); } } diff --git a/src/main/java/net/minecraftforge/coremod/CoreModTracker.java b/src/main/java/net/minecraftforge/coremod/CoreModTracker.java index 633705c..9026d3b 100644 --- a/src/main/java/net/minecraftforge/coremod/CoreModTracker.java +++ b/src/main/java/net/minecraftforge/coremod/CoreModTracker.java @@ -9,38 +9,77 @@ import javax.script.ScriptException; import java.io.IOException; +/** + * Tracks the current coremod being processed. + */ public class CoreModTracker { - private static ThreadLocal coreModThreadLocal = ThreadLocal.withInitial(CoreModTracker::new); + private static final ThreadLocal LOCAL = ThreadLocal.withInitial(CoreModTracker::new); private CoreMod tracked; + /** + * Sets the coremod currently being processed. + * + * @param coreMod The coremod to track + */ public static void setCoreMod(CoreMod coreMod) { - coreModThreadLocal.get().tracked = coreMod; + LOCAL.get().tracked = coreMod; } + /** + * Clears the coremod currently being processed. + */ public static void clearCoreMod() { - coreModThreadLocal.get().tracked = null; + LOCAL.get().tracked = null; } + /** + * Loads a script file by name. This will be loaded relative to the coremod's path. + * + * @param file The file to load + * @return True if the file was loaded, false otherwise + * + * @throws ScriptException If the script engine encounters an error, usually due to a syntax error in the script + * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file + * @see net.minecraftforge.coremod.api.ASMAPI#loadFile(String) + */ public static boolean loadFileByName(final String file) throws ScriptException, IOException { - final CoreMod tracked = coreModThreadLocal.get().tracked; + final CoreMod tracked = LOCAL.get().tracked; if (tracked != null) { return tracked.loadAdditionalFile(file); } return false; } + /** + * Loads a JSON data file by name. This will be loaded relative to the coremod's path. + * + * @param file The file to load + * @return The loaded JSON data if successful, or {@code null} if not + * + * @throws ScriptException If the parsed JSON data is malformed + * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file + * @see net.minecraftforge.coremod.api.ASMAPI#loadData(String) + */ @Nullable public static Object loadDataByName(final String file) throws ScriptException, IOException { - final CoreMod tracked = coreModThreadLocal.get().tracked; + final CoreMod tracked = LOCAL.get().tracked; if (tracked != null) { return tracked.loadAdditionalData(file); } return null; } + /** + * Logs a message from the coremod. + * + * @param level The log level + * @param message The message + * @param args Any formatting arguments + * @see net.minecraftforge.coremod.api.ASMAPI#log(String, String, Object...) + */ public static void log(final String level, final String message, final Object[] args) { - final CoreMod tracked = coreModThreadLocal.get().tracked; + final CoreMod tracked = LOCAL.get().tracked; if (tracked != null) { tracked.logMessage(level, message, args); } diff --git a/src/main/java/net/minecraftforge/coremod/NashornFactory.java b/src/main/java/net/minecraftforge/coremod/NashornFactory.java index 7b93d7c..f9eae1b 100644 --- a/src/main/java/net/minecraftforge/coremod/NashornFactory.java +++ b/src/main/java/net/minecraftforge/coremod/NashornFactory.java @@ -4,21 +4,36 @@ */ package net.minecraftforge.coremod; -import java.util.function.Function; +import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory; +import org.openjdk.nashorn.api.scripting.ScriptObjectMirror; import javax.script.Bindings; import javax.script.ScriptEngine; - -import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory; -import org.openjdk.nashorn.api.scripting.ScriptObjectMirror; +import java.util.Objects; +import java.util.function.Function; class NashornFactory { + // TODO for CoreMods 5.3 or 6.0: Consider args that improve performance + // https://github.com/openjdk/nashorn/blob/2eb88e4024023ee8e9baacb7736f914e3aa68aa4/src/org.openjdk.nashorn/share/classes/org/openjdk/nashorn/internal/runtime/resources/Options.properties + private static final String[] NASHORN_ARGS = new String[] { + "--language=es6" + }; + static ScriptEngine createEngine() { - return new NashornScriptEngineFactory().getScriptEngine(CoreModEngine::checkClass); + return new NashornScriptEngineFactory().getScriptEngine(NASHORN_ARGS, getAppClassLoader(), CoreModEngine::checkClass); + } + + /** @see NashornScriptEngineFactory#getAppClassLoader() */ + @SuppressWarnings("JavadocReference") + private static ClassLoader getAppClassLoader() { + return Objects.requireNonNullElseGet( + Thread.currentThread().getContextClassLoader(), + NashornScriptEngineFactory.class::getClassLoader + ); } @SuppressWarnings("unchecked") - static Function getFunction(Bindings obj) { - return a -> (R)((ScriptObjectMirror)obj).call(obj, a); + static Function getFunction(Bindings obj) { + return a -> (R) ((ScriptObjectMirror) obj).call(obj, a); } } diff --git a/src/main/java/net/minecraftforge/coremod/api/ASMAPI.java b/src/main/java/net/minecraftforge/coremod/api/ASMAPI.java index 1ae3424..e310141 100644 --- a/src/main/java/net/minecraftforge/coremod/api/ASMAPI.java +++ b/src/main/java/net/minecraftforge/coremod/api/ASMAPI.java @@ -20,196 +20,166 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.ListIterator; import java.util.Objects; import java.util.Optional; import java.util.function.Function; /** - * Helper methods for working with ASM. + * Helper methods for working with ASM. The goal of this class is to provide several assisting methods for common tasks + * to prevent boilerplate code, excessive imports, unnecessary loops, and to provide a more user-friendly API for + * coremod developers. */ +@SuppressWarnings("unused") // annoying IDE warnings public class ASMAPI { - public static MethodNode getMethodNode() { - return new MethodNode(Opcodes.ASM9); - } + /* BUILDING INSTRUCTION LISTS */ /** - * Injects a method call to the beginning of the given method. + * Builds a new {@link InsnList} out of the specified {@link AbstractInsnNode}s. * - * @param node The method to inject the call into - * @param methodCall The method call to inject + * @param nodes The instructions you want to add + * @return A new list with the instructions */ - public static void injectMethodCall(MethodNode node, MethodInsnNode methodCall) { - node.instructions.insertBefore(node.instructions.getFirst(), methodCall); + public static InsnList listOf(AbstractInsnNode... nodes) { + InsnList list = new InsnList(); + for (AbstractInsnNode node : nodes) list.add(node); + return list; } + + /* INSTRUCTION INJECTION */ + /** - * Injects a method call to the beginning of the given method. - * - * @param node The method to inject the call into - * @param methodCall The method call to inject - * - * @deprecated Renamed to {@link #injectMethodCall(MethodNode, MethodInsnNode)} + * The mode in which the given code should be inserted. */ - @Deprecated(forRemoval = true, since = "6.0") - public static void appendMethodCall(MethodNode node, MethodInsnNode methodCall) { - injectMethodCall(node, methodCall); + public enum InsertMode { + INSERT_BEFORE, INSERT_AFTER, REMOVE_ORIGINAL } /** - * Signifies the method invocation type. Mirrors "INVOKE-" opcodes from ASM. + * Inserts/replaces an instruction, with respect to the given {@link InsertMode}, on the given instruction. + * + * @param method The method to insert the instruction into + * @param insn The old instruction where the new instruction should be inserted into + * @param toInsert The instruction to be inserted + * @param mode How the instruction should be inserted + * @return {@code true} if the list was inserted, {@code false} otherwise */ - public enum MethodType { - VIRTUAL(Opcodes.INVOKEVIRTUAL), - SPECIAL(Opcodes.INVOKESPECIAL), - STATIC(Opcodes.INVOKESTATIC), - INTERFACE(Opcodes.INVOKEINTERFACE), - DYNAMIC(Opcodes.INVOKEDYNAMIC); - - private final int opcode; + public static boolean insertInsn(MethodNode method, AbstractInsnNode insn, AbstractInsnNode toInsert, InsertMode mode) { + if (!method.instructions.contains(insn)) return false; - MethodType(int opcode) { - this.opcode = opcode; + switch (mode) { + case INSERT_BEFORE -> method.instructions.insertBefore(insn, toInsert); + case INSERT_AFTER -> method.instructions.insert(insn, toInsert); + case REMOVE_ORIGINAL -> method.instructions.set(insn, toInsert); } - public int toOpcode() { - return this.opcode; - } + return true; } /** - * Builds a new {@link MethodInsnNode} with the given parameters. The opcode of the method call is determined by the - * given {@link MethodType}. + * Inserts/replaces an instruction, with respect to the given {@link InsertMode}, on the first + * {@link MethodInsnNode} that matches the parameters of these functions in the method provided. Only the first + * matching node is targeted, all other matches are ignored. * - * @param ownerName The method owner (class) - * @param methodName The method name - * @param methodDescriptor The method descriptor - * @param type The type of method call - * @return The built method call node - */ - public static MethodInsnNode buildMethodCall(final String ownerName, final String methodName, final String methodDescriptor, final MethodType type) { - return new MethodInsnNode(type.toOpcode(), ownerName, methodName, methodDescriptor, type == MethodType.INTERFACE); - } - - /** - * Builds a new {@link MethodInsnNode} with the given parameters. The opcode of the method call is determined by the - * given {@link MethodType}. + * @param method The method to insert the instruction into + * @param type The type of the method call to search for + * @param owner The owner of the method call to search for + * @param name The name of the method call to search for (you may want to use {@link #mapMethod(String)} if this + * is a srg name) + * @param desc The desc of the method call to search for + * @param toInsert The instruction to be inserted + * @param mode How the instruction should be inserted + * @return {@code true} if the method call was found and the list was inserted, {@code false} otherwise * - * @param type The type of method call - * @param ownerName The method owner (class) - * @param methodName The method name - * @param methodDescriptor The method descriptor - * @return The built method call node + * @apiNote This method mostly exists for the sake of backwards-compatibility. It may be prudent to use + * {@link #insertInsn(MethodNode, AbstractInsnNode, AbstractInsnNode, InsertMode)} instead if you want to value + * readability. */ - public static MethodInsnNode buildMethodCall(final MethodType type, final String ownerName, final String methodName, final String methodDescriptor) { - return new MethodInsnNode(type.toOpcode(), ownerName, methodName, methodDescriptor, type == MethodType.INTERFACE); - } + public static boolean insertInsn(MethodNode method, MethodType type, String owner, String name, String desc, AbstractInsnNode toInsert, InsertMode mode) { + var insn = findFirstMethodCall(method, type, owner, name, desc); + if (insn == null) return false; - public static FieldInsnNode buildFieldCall(final int opcode, final String owner, final String name, final String desc) { - return new FieldInsnNode(opcode, owner, name, desc); + return insertInsn(method, insn, toInsert, mode); } /** - * Signifies the type of number constant for a {@link NumberType}. + * Inserts/replaces an instruction list, with respect to the given {@link InsertMode}, on the given instruction. + * + * @param method The method to insert the list into + * @param list The list to be inserted + * @param mode How the list should be inserted + * @return {@code true} if the list was inserted, {@code false} otherwise */ - public enum NumberType { - INTEGER(Number::intValue), - FLOAT(Number::floatValue), - LONG(Number::longValue), - DOUBLE(Number::doubleValue); + public static boolean insertInsnList(MethodNode method, AbstractInsnNode insn, InsnList list, InsertMode mode) { + if (!method.instructions.contains(insn)) return false; - private final Function mapper; + if (mode == InsertMode.INSERT_BEFORE) + method.instructions.insertBefore(insn, list); + else + method.instructions.insert(insn, list); - NumberType(Function mapper) { - this.mapper = mapper; - } + if (mode == InsertMode.REMOVE_ORIGINAL) + method.instructions.remove(insn); - public Object map(Number number) { - return mapper.apply(number); - } + return true; } /** - * Casts a given number to a given specific {@link NumberType}. This helps elliviate the problems that comes with JavaScript's - * ambiguous number system. - *

- * The result is returned as an {@link Object} so it can be used as a value in various instructions that require - * values. + * Inserts/replaces an instruction list, with respect to the given {@link InsertMode}, on the first + * {@link MethodInsnNode} that matches the parameters of these functions in the method provided. Only the first + * matching node is targeted, all other matches are ignored. * - * @param value The number to cast - * @param type The type of number to cast to - * @return The casted number - */ - public static Object castNumber(final Number value, final NumberType type) { - return type.map(value); - } - - /** - * Builds a new {@link LdcInsnNode} with the given number value and {@link NumberType}. + * @param method The method to insert the list into + * @param type The type of the method call to search for + * @param owner The owner of the method call to search for + * @param name The name of the method call to search for (you may want to use {@link #mapMethod(String)} if this + * is a srg name) + * @param desc The desc of the method call to search for + * @param list The list to be inserted + * @param mode How the list should be inserted + * @return {@code true} if the method call was found and the list was inserted, {@code false} otherwise * - * @param value The number value - * @param type The type of the number - * @return The built LDC node + * @apiNote This method mostly exists for the sake of backwards-compatibility. It may be prudent to use + * {@link #insertInsnList(MethodNode, AbstractInsnNode, InsnList, InsertMode)} instead if you want to value + * readability. */ - public static LdcInsnNode buildNumberLdcInsnNode(final Number value, final NumberType type) { - return new LdcInsnNode(castNumber(value, type)); - } + public static boolean insertInsnList(MethodNode method, MethodType type, String owner, String name, String desc, InsnList list, InsertMode mode) { + var insn = findFirstMethodCall(method, type, owner, name, desc); + if (insn == null) return false; - /** - * Maps a method from the given SRG name to the mapped name at deobfuscated runtime. - * - * @param name The SRG name of the method - * @return The mapped name of the method - * - * @apiNote As of Minecraft 1.20.4, Forge no longer uses SRG names in production. While the mapping system will - * still work for sake of backwards-compatibility, you should not be using this method if you are on 1.20.4 or - * later. - */ - public static String mapMethod(String name) { - return map(name, INameMappingService.Domain.METHOD); + return insertInsnList(method, insn, list, mode); } /** - * Maps a field from the given SRG name to the mapped name at deobfuscated runtime. - * - * @param name The SRG name of the field - * @return The mapped name of the field + * Injects a method call to the beginning of the given method. * - * @apiNote As of Minecraft 1.20.4, Forge no longer uses SRG names in production. While the mapping system will - * still work for sake of backwards-compatibility, you should not be using this method if you are on 1.20.4 or - * later. + * @param method The method to inject the method call into + * @param insn The method call to inject */ - public static String mapField(String name) { - return map(name, INameMappingService.Domain.FIELD); - } - - private static String map(String name, INameMappingService.Domain domain) { - return Optional.ofNullable(Launcher.INSTANCE). - map(Launcher::environment). - flatMap(env -> env.findNameMapping("srg")). - map(f -> f.apply(domain, name)).orElse(name); + public static void injectMethodCall(MethodNode method, MethodInsnNode insn) { + method.instructions.insertBefore(method.instructions.getFirst(), insn); } /** - * Checks if the given JVM property (or if the property prepended with {@code "coremod."}) is {@code true}. + * Injects a method call to the beginning of the given method. * - * @param propertyName the property to check - * @return true if the property is true + * @param method The method to inject the call into + * @param insn The method call to inject + * @deprecated Renamed to {@link #injectMethodCall(MethodNode, MethodInsnNode)} */ - public static boolean getSystemPropertyFlag(final String propertyName) { - return Boolean.getBoolean(propertyName) || Boolean.getBoolean("coremod." + propertyName); + @Deprecated(forRemoval = true, since = "6.0") + public static void appendMethodCall(MethodNode method, MethodInsnNode insn) { + injectMethodCall(method, insn); } - /** - * The mode in which the given code should be inserted. - */ - public enum InsertMode { - REMOVE_ORIGINAL, INSERT_BEFORE, INSERT_AFTER - } + + /* INSTRUCTION SEARCHING */ /** - * The type of instruction. Useful for searching for a specfic instruction, and is preferred over checking the - * opcode or other equivalent. + * The type of instruction. Useful for searching for a specfic instruction, especially for those that use opcode + * {@code -1} or you need to ignore matching the opcode. * * @see AbstractInsnNode */ @@ -237,6 +207,11 @@ public enum InsnType { this.type = type; } + /** + * Gets the type of the instruction represented in {@link AbstractInsnNode}. + * + * @return The type + */ public int get() { return type; } @@ -246,78 +221,78 @@ public int get() { * Finds the first instruction with matching opcode. * * @param method the method to search in - * @param opCode the opcode to search for - * @return the found instruction node or null if none matched + * @param opcode the opcode to search for + * @return the found instruction or {@code null} if none matched */ - public static AbstractInsnNode findFirstInstruction(MethodNode method, int opCode) { - return findFirstInstructionAfter(method, opCode, null, 0); + public static @Nullable AbstractInsnNode findFirstInstruction(MethodNode method, int opcode) { + return findFirstInstructionAfter(method, opcode, null, 0); } /** - * Finds the first instruction with matching opcode. + * Finds the first instruction with matching instruction type. * * @param method the method to search in * @param type the instruction type to search for - * @return the found instruction node or null if none matched + * @return the found instruction node or {@code null} if none matched */ - public static AbstractInsnNode findFirstInstruction(MethodNode method, InsnType type) { + public static @Nullable AbstractInsnNode findFirstInstruction(MethodNode method, InsnType type) { return findFirstInstructionAfter(method, -2, type, 0); } /** - * Finds the first instruction with matching opcode. + * Finds the first instruction with matching opcode and instruction type. * * @param method the method to search in - * @param opCode the opcode to search for + * @param opcode the opcode to search for * @param type the instruction type to search for - * @return the found instruction node or null if none matched + * @return the found instruction node or {@code null} if none matched */ - public static AbstractInsnNode findFirstInstruction(MethodNode method, int opCode, InsnType type) { - return findFirstInstructionAfter(method, opCode, type, 0); + public static @Nullable AbstractInsnNode findFirstInstruction(MethodNode method, int opcode, InsnType type) { + return findFirstInstructionAfter(method, opcode, type, 0); } /** * Finds the first instruction with matching opcode after the given start index. * * @param method the method to search in - * @param opCode the opcode to search for + * @param opcode the opcode to search for * @param startIndex the index to start search after (inclusive) - * @return the found instruction node or null if none matched after the given index + * @return the found instruction node or {@code null} if none matched after the given index */ - public static AbstractInsnNode findFirstInstructionAfter(MethodNode method, int opCode, int startIndex) { - return findFirstInstructionAfter(method, opCode, null, startIndex); + public static @Nullable AbstractInsnNode findFirstInstructionAfter(MethodNode method, int opcode, int startIndex) { + return findFirstInstructionAfter(method, opcode, null, startIndex); } /** - * Finds the first instruction with matching opcode after the given start index. + * Finds the first instruction with matching instruction type after the given start index. * * @param method the method to search in * @param type the instruction type to search for * @param startIndex the index to start search after (inclusive) - * @return the found instruction node or null if none matched after the given index + * @return the found instruction node or {@code null} if none matched after the given index */ - public static AbstractInsnNode findFirstInstructionAfter(MethodNode method, InsnType type, int startIndex) { + public static @Nullable AbstractInsnNode findFirstInstructionAfter(MethodNode method, InsnType type, int startIndex) { return findFirstInstructionAfter(method, -2, type, startIndex); } /** - * Finds the first instruction with matching opcode after the given start index + * Finds the first instruction with matching opcode and instruction type after the given start index. * * @param method the method to search in - * @param opCode the opcode to search for + * @param opcode the opcode to search for * @param type the instruction type to search for * @param startIndex the index to start search after (inclusive) - * @return the found instruction node or null if none matched after the given index + * @return the found instruction node or {@code null} if none matched after the given index */ - public static AbstractInsnNode findFirstInstructionAfter(MethodNode method, int opCode, @Nullable InsnType type, int startIndex) { - boolean checkType = type != null; + public static @Nullable AbstractInsnNode findFirstInstructionAfter(MethodNode method, int opcode, @Nullable InsnType type, int startIndex) { for (int i = Math.max(0, startIndex); i < method.instructions.size(); i++) { AbstractInsnNode ain = method.instructions.get(i); - boolean opcodeMatch = opCode < -1 || ain.getOpcode() == opCode; - boolean typeMatch = !checkType || type.get() == ain.getType(); + boolean opcodeMatch = opcode < -1 || ain.getOpcode() == opcode; + boolean typeMatch = type == null || type.get() == ain.getType(); if (opcodeMatch && typeMatch) return ain; } + return null; } @@ -325,33 +300,33 @@ public static AbstractInsnNode findFirstInstructionAfter(MethodNode method, int * Finds the first instruction with matching opcode before the given index in reverse search. * * @param method the method to search in - * @param opCode the opcode to search for + * @param opcode the opcode to search for * @param startIndex the index at which to start searching (inclusive) - * @return the found instruction node or null if none matched before the given startIndex + * @return the found instruction node or {@code null} if none matched before the given startIndex * - * @apiNote In Minecraft 1.21.1 and earlier, this method contains broken logic that ignores the {@code startIndex} - * parameter and searches for the requested instruction at the end of the method. This behavior is preserved to - * not disrupt older coremods. If you are on one of these older versions and need to use the fixed logic, please - * use {@link #findFirstInstructionBefore(MethodNode, int, int, boolean)}. + * @apiNote In Minecraft 1.21.1 and earlier, this method contains broken logic that ignores the + * {@code startIndex} parameter and searches for the requested instruction at the end of the method. This + * behavior is preserved to not disrupt older coremods. If you are on one of these older versions and need to + * use the fixed logic, please use {@link #findFirstInstructionBefore(MethodNode, int, int, boolean)}. */ - public static AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opCode, int startIndex) { - return findFirstInstructionBefore(method, opCode, null, startIndex); + public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opcode, int startIndex) { + return findFirstInstructionBefore(method, opcode, null, startIndex); } /** - * Finds the first instruction with matching opcode before the given index in reverse search. + * Finds the first instruction with matching instruction type before the given index in reverse search. * * @param method the method to search in * @param type the instruction type to search for * @param startIndex the index at which to start searching (inclusive) - * @return the found instruction node or null if none matched before the given startIndex + * @return the found instruction node or {@code null} if none matched before the given startIndex * - * @apiNote In Minecraft 1.21.1 and earlier, this method contains broken logic that ignores the {@code startIndex} - * parameter and searches for the requested instruction at the end of the method. This behavior is preserved to - * not disrupt older coremods. If you are on one of these older versions and need to use the fixed logic, please - * use {@link #findFirstInstructionBefore(MethodNode, int, int, boolean)}. + * @apiNote In Minecraft 1.21.1 and earlier, this method contains broken logic that ignores the + * {@code startIndex} parameter and searches for the requested instruction at the end of the method. This + * behavior is preserved to not disrupt older coremods. If you are on one of these older versions and need to + * use the fixed logic, please use {@link #findFirstInstructionBefore(MethodNode, int, int, boolean)}. */ - public static AbstractInsnNode findFirstInstructionBefore(MethodNode method, InsnType type, int startIndex) { + public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, InsnType type, int startIndex) { return findFirstInstructionBefore(method, -2, type, startIndex); } @@ -361,64 +336,65 @@ public static AbstractInsnNode findFirstInstructionBefore(MethodNode method, Ins * @param method the method to search in * @param opCode the opcode to search for * @param startIndex the index at which to start searching (inclusive) - * @param fixLogic whether to use the fixed logic for finding instructions before the given startIndex (true by - * default on versions since 1.21.3, false otherwise) - * @return the found instruction node or null if none matched before the given startIndex + * @param fixLogic whether to use the fixed logic for finding instructions before the given startIndex + * ({@code true} by default on versions since 1.21.3, {@code false} otherwise) + * @return the found instruction node or {@code null} if none matched before the given startIndex */ - public static AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opCode, int startIndex, boolean fixLogic) { + public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opCode, int startIndex, boolean fixLogic) { return findFirstInstructionBefore(method, opCode, null, startIndex, fixLogic); } /** - * Finds the first instruction with matching opcode before the given index in reverse search. + * Finds the first instruction with matching instruction type before the given index in reverse search. * * @param method the method to search in * @param type the instruction type to search for * @param startIndex the index at which to start searching (inclusive) - * @param fixLogic whether to use the fixed logic for finding instructions before the given startIndex (true by - * default on versions since 1.21.3, false otherwise) - * @return the found instruction node or null if none matched before the given startIndex + * @param fixLogic whether to use the fixed logic for finding instructions before the given startIndex + * ({@code true} by default on versions since 1.21.3, {@code false} otherwise) + * @return the found instruction node or {@code null} if none matched before the given startIndex */ - public static AbstractInsnNode findFirstInstructionBefore(MethodNode method, InsnType type, int startIndex, boolean fixLogic) { + public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, InsnType type, int startIndex, boolean fixLogic) { return findFirstInstructionBefore(method, -2, type, startIndex, fixLogic); } /** - * Finds the first instruction with matching opcode before the given index in reverse search + * Finds the first instruction with matching opcode and instruction type before the given index in reverse search. * * @param method the method to search in * @param opCode the opcode to search for * @param startIndex the index at which to start searching (inclusive) - * @return the found instruction node or null if none matched before the given startIndex + * @return the found instruction node or {@code null} if none matched before the given startIndex * - * @apiNote In Minecraft 1.21.1 and earlier, this method contains broken logic that ignores the {@code startIndex} - * parameter and searches for the requested instruction at the end of the method. This behavior is preserved to - * not disrupt older coremods. If you are on one of these older versions and need to use the fixed logic, please - * use {@link #findFirstInstructionBefore(MethodNode, int, InsnType, int, boolean)}. + * @apiNote In Minecraft 1.21.1 and earlier, this method contains broken logic that ignores the + * {@code startIndex} parameter and searches for the requested instruction at the end of the method. This + * behavior is preserved to not disrupt older coremods. If you are on one of these older versions and need to + * use the fixed logic, please use + * {@link #findFirstInstructionBefore(MethodNode, int, InsnType, int, boolean)}. */ - public static AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opCode, @Nullable InsnType type, int startIndex) { + public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opCode, @Nullable InsnType type, int startIndex) { return findFirstInstructionBefore(method, opCode, type, startIndex, !CoreModEngine.DO_NOT_FIX_INSNBEFORE); } /** - * Finds the first instruction with matching opcode before the given index in reverse search + * Finds the first instruction with matching opcode and instruction type before the given index in reverse search. * * @param method the method to search in * @param opCode the opcode to search for * @param startIndex the index at which to start searching (inclusive) - * @param fixLogic whether to use the fixed logic for finding instructions before the given startIndex (true by - * default on versions since 1.21.3, false otherwise) - * @return the found instruction node or null if none matched before the given startIndex + * @param fixLogic whether to use the fixed logic for finding instructions before the given startIndex + * ({@code true} by default on versions since 1.21.3, {@code false} otherwise) + * @return the found instruction node or {@code null} if none matched before the given startIndex */ - public static AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opCode, @Nullable InsnType type, int startIndex, boolean fixLogic) { - boolean checkType = type != null; + public static @Nullable AbstractInsnNode findFirstInstructionBefore(MethodNode method, int opCode, @Nullable InsnType type, int startIndex, boolean fixLogic) { for (int i = fixLogic ? Math.min(method.instructions.size() - 1, startIndex) : startIndex; i >= 0; i--) { AbstractInsnNode ain = method.instructions.get(i); boolean opcodeMatch = opCode < -1 || ain.getOpcode() == opCode; - boolean typeMatch = !checkType || type.get() == ain.getType(); + boolean typeMatch = type == null || type.get() == ain.getType(); if (opcodeMatch && typeMatch) return ain; } + return null; } @@ -430,9 +406,9 @@ public static AbstractInsnNode findFirstInstructionBefore(MethodNode method, int * @param owner the method call's owner to search for * @param name the method call's name * @param descriptor the method call's descriptor - * @return the found method call node or null if none matched + * @return the found method call node or {@code null} if none matched */ - public static MethodInsnNode findFirstMethodCall(MethodNode method, MethodType type, String owner, String name, String descriptor) { + public static @Nullable MethodInsnNode findFirstMethodCall(MethodNode method, MethodType type, String owner, String name, String descriptor) { return findFirstMethodCallAfter(method, type, owner, name, descriptor, 0); } @@ -445,22 +421,20 @@ public static MethodInsnNode findFirstMethodCall(MethodNode method, MethodType t * @param owner the method call's owner to search for * @param name the method call's name * @param descriptor the method call's descriptor - * @param startIndex the index after which to start searching (inclusive) - * @return the found method call node, null if none matched after the given index + * @param index the index after which to start searching (inclusive) + * @return the found method call node, {@code null} if none matched after the given index */ - public static MethodInsnNode findFirstMethodCallAfter(MethodNode method, MethodType type, String owner, String name, String descriptor, int startIndex) { - for (int i = Math.max(0, startIndex); i < method.instructions.size(); i++) { - AbstractInsnNode node = method.instructions.get(i); - if (node instanceof MethodInsnNode && - node.getOpcode() == type.toOpcode()) { - MethodInsnNode methodInsnNode = (MethodInsnNode) node; - if (methodInsnNode.owner.equals(owner) && - methodInsnNode.name.equals(name) && - methodInsnNode.desc.equals(descriptor)) { - return methodInsnNode; - } + public static @Nullable MethodInsnNode findFirstMethodCallAfter(MethodNode method, MethodType type, String owner, String name, String descriptor, int index) { + for (int i = Math.max(0, index); i < method.instructions.size(); i++) { + if (method.instructions.get(i) instanceof MethodInsnNode insn + && insn.getOpcode() == type.toOpcode() + && Objects.equals(insn.owner, owner) + && Objects.equals(insn.name, name) + && Objects.equals(insn.desc, descriptor)) { + return insn; } } + return null; } @@ -473,41 +447,39 @@ public static MethodInsnNode findFirstMethodCallAfter(MethodNode method, MethodT * @param owner the method call's owner to search for * @param name the method call's name * @param descriptor the method call's descriptor - * @param startIndex the index at which to start searching (inclusive) - * @return the found method call node or null if none matched before the given startIndex + * @param index the index at which to start searching (inclusive) + * @return the found method call node or {@code null} if none matched before the given startIndex */ - public static MethodInsnNode findFirstMethodCallBefore(MethodNode method, MethodType type, String owner, String name, String descriptor, int startIndex) { - for (int i = Math.min(method.instructions.size() - 1, startIndex); i >= 0; i--) { - AbstractInsnNode node = method.instructions.get(i); - if (node instanceof MethodInsnNode && - node.getOpcode() == type.toOpcode()) { - MethodInsnNode methodInsnNode = (MethodInsnNode) node; - if (methodInsnNode.owner.equals(owner) && - methodInsnNode.name.equals(name) && - methodInsnNode.desc.equals(descriptor)) { - return methodInsnNode; - } + public static @Nullable MethodInsnNode findFirstMethodCallBefore(MethodNode method, MethodType type, String owner, String name, String descriptor, int index) { + for (int i = Math.min(method.instructions.size() - 1, index); i >= 0; i--) { + if (method.instructions.get(i) instanceof MethodInsnNode insn + && insn.getOpcode() == type.toOpcode() + && Objects.equals(insn.owner, owner) + && Objects.equals(insn.name, name) + && Objects.equals(insn.desc, descriptor)) { + return insn; } } + return null; } /** - * Finds the first method call in the given method matching the given type, owner, name and descriptor. + * Finds the first field call in the given method matching the given opcode, owner, name and descriptor. * * @param method the method to search in * @param opcode the opcode of field call to search for * @param owner the method call's owner to search for * @param name the method call's name * @param descriptor the method call's descriptor - * @return the found method call node or null if none matched + * @return the found method call node or {@code null} if none matched */ public static @Nullable FieldInsnNode findFirstFieldCall(MethodNode method, int opcode, String owner, String name, String descriptor) { return findFirstFieldCallAfter(method, opcode, owner, name, descriptor, 0); } /** - * Finds the first method call in the given method matching the given type, owner, name and descriptor after the + * Finds the first field call in the given method matching the given opcode, owner, name and descriptor after the * instruction given index. * * @param method the method to search in @@ -516,7 +488,7 @@ public static MethodInsnNode findFirstMethodCallBefore(MethodNode method, Method * @param name the method call's name * @param descriptor the method call's descriptor * @param startIndex the index after which to start searching (inclusive) - * @return the found method call node, null if none matched after the given index + * @return the found method call node, {@code null} if none matched after the given index */ public static @Nullable FieldInsnNode findFirstFieldCallAfter(MethodNode method, int opcode, String owner, String name, String descriptor, int startIndex) { for (int i = Math.max(0, startIndex); i < method.instructions.size(); i++) { @@ -532,7 +504,7 @@ public static MethodInsnNode findFirstMethodCallBefore(MethodNode method, Method } /** - * Finds the first method call in the given method matching the given type, owner, name and descriptor before the + * Finds the first field call in the given method matching the given opcode, owner, name and descriptor before the * given index in reverse search. * * @param method the method to search in @@ -541,7 +513,7 @@ public static MethodInsnNode findFirstMethodCallBefore(MethodNode method, Method * @param name the method call's name * @param descriptor the method call's descriptor * @param startIndex the index at which to start searching (inclusive) - * @return the found method call node or null if none matched before the given startIndex + * @return the found method call node or {@code null} if none matched before the given startIndex */ public static @Nullable FieldInsnNode findFirstFieldCallBefore(MethodNode method, int opcode, String owner, String name, String descriptor, int startIndex) { for (int i = Math.min(method.instructions.size() - 1, startIndex); i >= 0; i--) { @@ -556,104 +528,333 @@ public static MethodInsnNode findFirstMethodCallBefore(MethodNode method, Method return null; } + + /* CREATING AND FINDING METHODS */ + /** - * Inserts/replaces a list after/before first {@link MethodInsnNode} that matches the parameters of these functions - * in the method provided. Only the first node matching is targeted, all other matches are ignored. + * Creates a new empty {@link MethodNode}. * - * @param method The method where you want to find the node - * @param type The type of the old method node - * @param owner The owner of the old method node - * @param name The name of the old method node (you may want to use {@link #mapMethod(String)} if this is a srg - * name) - * @param desc The desc of the old method node - * @param list The list that should be inserted - * @param mode How the given code should be inserted - * @return True if the node was found and the list was inserted, false otherwise + * @return The created method node + * + * @see MethodNode#MethodNode(int) */ - public static boolean insertInsnList(MethodNode method, MethodType type, String owner, String name, String desc, InsnList list, InsertMode mode) { - var insn = findFirstMethodCall(method, type, owner, name, desc); - if (insn == null) return false; + public static MethodNode getMethodNode() { + var method = new MethodNode(Opcodes.ASM9); - return insertInsnList(method, insn, list, mode); + // ASM usually creates an empty list for null exceptions on the other constructors + // let's do this as well, just to make sure we don't run into problems later. + method.exceptions = new ArrayList<>(); + + return method; } /** - * Inserts/replaces a list after/before the given instruction. + * Creates a new empty {@link MethodNode} with the given access codes, name and descriptor. * - * @param method The method where you want to insert the list - * @param list The list that should be inserted - * @param mode How the given code should be inserted - * @return True if the list was inserted, false otherwise + * @param access The access codes + * @param name The method name + * @param descriptor The method descriptor + * @return The created method node + * + * @see MethodNode#MethodNode(int, int, String, String, String, String[]) */ - public static boolean insertInsnList(MethodNode method, AbstractInsnNode insn, InsnList list, InsertMode mode) { - if (!method.instructions.contains(insn)) return false; + public static MethodNode getMethodNode(int access, String name, String descriptor) { + return new MethodNode(Opcodes.ASM9, access, name, descriptor, null, null); + } - if (mode == InsertMode.INSERT_BEFORE) - method.instructions.insertBefore(insn, list); - else - method.instructions.insert(insn, list); + /** + * Creates a new empty {@link MethodNode} with the given access codes, name, descriptor, and signature. + * + * @param access The access codes + * @param name The method name + * @param descriptor The method descriptor + * @param signature The method signature + * @return The created method node + * + * @see MethodNode#MethodNode(int, int, String, String, String, String[]) + */ + public static MethodNode getMethodNode(int access, String name, String descriptor, @Nullable String signature) { + return new MethodNode(Opcodes.ASM9, access, name, descriptor, signature, null); + } - if (mode == InsertMode.REMOVE_ORIGINAL) - method.instructions.remove(insn); + /** + * Creates a new empty {@link MethodNode} with the given access codes, name, descriptor, signature, and exceptions. + * + * @param access The access codes + * @param name The method name + * @param descriptor The method descriptor + * @param signature The method signature + * @param exceptions The internal names of the method's exceptions + * @return The created method node + * + * @see MethodNode#MethodNode(int, int, String, String, String, String[]) + */ + public static MethodNode getMethodNode(int access, String name, String descriptor, @Nullable String signature, @Nullable String[] exceptions) { + return new MethodNode(Opcodes.ASM9, access, name, descriptor, signature, exceptions); + } - return true; + /** + * Finds the first method node from the given class node that matches the given name and descriptor. + * + * @param clazz The class node to search + * @param name The name of the desired method + * @param desc The descriptor of the desired method + * @return The found method node or {@code null} if none matched + */ + public static @Nullable MethodNode findMethodNode(ClassNode clazz, String name, String desc) { + return findMethodNode(clazz, name, desc, null, false); } /** - * Inserts/replaces an instruction after/before first {@link MethodInsnNode} that matches the parameters of these - * functions in the method provided. Only the first node matching is targeted, all other matches are ignored. + * Finds the first method node from the given class node that matches the given name, descriptor, and signature. + * + * @param clazz The class node to search + * @param name The name of the desired method + * @param desc The descriptor of the desired method + * @param signature The signature of the desired method + * @return The found method node or {@code null} if none matched * - * @param method The method where you want to find the node - * @param type The type of the old method node - * @param owner The owner of the old method node - * @param name The name of the old method node (you may want to use {@link #mapMethod(String)} if this is a srg - * name) - * @param desc The desc of the old method node - * @param toInsert The instruction that should be inserted - * @param mode How the given code should be inserted - * @return True if the node was found and the list was inserted, false otherwise + * @apiNote This method will attempt to match the signature of the method, even if it is {@code null}. It may be + * useful for that use case in particular. If you have no need to match the signature, consider using + * {@link #findMethodNode(ClassNode, String, String)} */ - public static boolean insertInsn(MethodNode method, MethodType type, String owner, String name, String desc, AbstractInsnNode toInsert, InsertMode mode) { - var insn = findFirstMethodCall(method, type, owner, name, desc); - if (insn == null) return false; + public static @Nullable MethodNode findMethodNode(ClassNode clazz, String name, String desc, @Nullable String signature) { + return findMethodNode(clazz, name, desc, signature, true); + } - return insertInsn(method, insn, toInsert, mode); + private static @Nullable MethodNode findMethodNode(ClassNode clazz, String name, String desc, @Nullable String signature, boolean checkSignature) { + for (MethodNode method : clazz.methods) { + // we have to use Objects.equals here in case the found method has null attributes + if (Objects.equals(method.name, name) && Objects.equals(method.desc, desc) && (!checkSignature || Objects.equals(method.signature, signature))) { + return method; + } + } + + return null; } + + /* CREATING AND FINDING FIELDS */ + /** - * Inserts/replaces an instruction after/before the given instruction. + * Creates a new empty {@link FieldNode} with the given access codes, name, and descriptor. * - * @param method The method where you want to insert the list - * @param insn The instruction where the new instruction should be inserted into - * @param toInsert The instruction that should be inserted - * @param mode How the given code should be inserted - * @return True if the list was inserted, false otherwise + * @param access The access codes + * @param name The field name + * @param descriptor The field descriptor + * @return The created field node + * + * @see FieldNode#FieldNode(int, int, String, String, String, Object) */ - public static boolean insertInsn(MethodNode method, AbstractInsnNode insn, AbstractInsnNode toInsert, InsertMode mode) { - if (!method.instructions.contains(insn)) return false; + public static FieldNode getFieldNode(int access, String name, String descriptor) { + return new FieldNode(Opcodes.ASM9, access, name, descriptor, null, null); + } - switch (mode) { - case INSERT_BEFORE -> method.instructions.insertBefore(insn, toInsert); - case INSERT_AFTER -> method.instructions.insert(insn, toInsert); - case REMOVE_ORIGINAL -> method.instructions.set(insn, toInsert); + /** + * Creates a new empty {@link FieldNode} with the given access codes, name, descriptor, and signature. + * + * @param access The access codes + * @param name The field name + * @param descriptor The field descriptor + * @param signature The field signature + * @return The created field node + * + * @see FieldNode#FieldNode(int, int, String, String, String, Object) + */ + public static FieldNode getFieldNode(int access, String name, String descriptor, @Nullable String signature) { + return new FieldNode(Opcodes.ASM9, access, name, descriptor, signature, null); + } + + /** + * Creates a new empty {@link FieldNode} with the given access codes, name, descriptor, signature, and initial + * object value. + * + * @param access The access codes + * @param name The field name + * @param descriptor The field descriptor + * @param signature The field signature + * @param value The initial value of the field + * @return The created field node + * + * @see FieldNode#FieldNode(int, int, String, String, String, Object) + */ + public static FieldNode getFieldNode(int access, String name, String descriptor, @Nullable String signature, String value) { + return new FieldNode(Opcodes.ASM9, access, name, descriptor, signature, value); + } + + /** + * Creates a new empty {@link FieldNode} with the given access codes, name, descriptor, signature, and initial + * number value. + * + * @param access The access codes + * @param name The field name + * @param descriptor The field descriptor + * @param signature The field signature + * @param value The initial value of the field + * @param valueType The number type of the initial value + * @return The created field node + * + * @see FieldNode#FieldNode(int, int, String, String, String, Object) + */ + public static FieldNode getFieldNode(int access, String name, String descriptor, @Nullable String signature, Number value, NumberType valueType) { + return new FieldNode(Opcodes.ASM9, access, name, descriptor, signature, castNumber(value, valueType)); + } + + /** + * Finds the first field node from the given class node that matches the given name and descriptor. + * + * @param clazz The class node to search + * @param name The name of the desired field + * @param desc The descriptor of the desired field + * @return The found field node or {@code null} if none matched + */ + public static @Nullable FieldNode findFieldNode(ClassNode clazz, String name, String desc) { + return findFieldNode(clazz, name, desc, null, false); + } + + /** + * Finds the first field node from the given class node that matches the given name, descriptor, and signature. + * + * @param clazz The class node to search + * @param name The name of the desired field + * @param desc The descriptor of the desired field + * @param signature The signature of the desired field + * @return The found field node or {@code null} if none matched + * + * @apiNote This method will attempt to match the signature of the field, even if it is {@code null}. It may be + * useful for that use case in particular. If you have no need to match the signature, consider using + * {@link #findFieldNode(ClassNode, String, String)} + */ + public static @Nullable FieldNode findFieldNode(ClassNode clazz, String name, String desc, @Nullable String signature) { + return findFieldNode(clazz, name, desc, signature, true); + } + + private static @Nullable FieldNode findFieldNode(ClassNode clazz, String name, String desc, @Nullable String signature, boolean checkSignature) { + for (FieldNode field : clazz.fields) { + // we have to use Objects.equals here in case the found field has null attributes + if (Objects.equals(field.name, name) && Objects.equals(field.desc, desc) && (!checkSignature || Objects.equals(field.signature, signature))) { + return field; + } } - return true; + return null; } + + /* BUILDING METHOD CALLS */ + /** - * Builds a new {@link InsnList} out of the specified {@link AbstractInsnNode}s. + * Signifies the method invocation type. Mirrors "INVOKE-" opcodes from ASM. + */ + public enum MethodType { + VIRTUAL(Opcodes.INVOKEVIRTUAL), + SPECIAL(Opcodes.INVOKESPECIAL), + STATIC(Opcodes.INVOKESTATIC), + INTERFACE(Opcodes.INVOKEINTERFACE), + DYNAMIC(Opcodes.INVOKEDYNAMIC); + + private final int opcode; + + MethodType(int opcode) { + this.opcode = opcode; + } + + /** + * Gets the opcode of the method type. + * + * @return The opcode + */ + public int toOpcode() { + return this.opcode; + } + } + + /** + * Builds a new {@link MethodInsnNode} with the given parameters. The opcode of the method call is determined by the + * given {@link MethodType}. * - * @param nodes The nodes you want to add - * @return A new list with the nodes + * @param type The type of method call + * @param ownerName The method owner (class) + * @param methodName The method name + * @param methodDescriptor The method descriptor + * @return The built method call node */ - public static InsnList listOf(AbstractInsnNode... nodes) { - InsnList list = new InsnList(); - for (AbstractInsnNode node : nodes) - list.add(node); - return list; + public static MethodInsnNode buildMethodCall(final MethodType type, final String ownerName, final String methodName, final String methodDescriptor) { + return new MethodInsnNode(type.toOpcode(), ownerName, methodName, methodDescriptor, type == MethodType.INTERFACE); + } + + /** + * Builds a new {@link MethodInsnNode} with the given parameters. The opcode of the method call is determined by the + * given {@link MethodType}. + * + * @param ownerName The method owner (class) + * @param methodName The method name + * @param methodDescriptor The method descriptor + * @param type The type of method call + * @return The built method call node + * + * @deprecated Use {@link #buildMethodCall(MethodType, String, String, String)} + */ + @Deprecated(forRemoval = true, since = "6.0") // when we major update, prefer the method type as first parameter + public static MethodInsnNode buildMethodCall(final String ownerName, final String methodName, final String methodDescriptor, final MethodType type) { + return new MethodInsnNode(type.toOpcode(), ownerName, methodName, methodDescriptor, type == MethodType.INTERFACE); } + + /* BUILDING FIELD CALLS */ + + public static FieldInsnNode buildFieldCall(final int opcode, final String owner, final String name, final String desc) { + return new FieldInsnNode(opcode, owner, name, desc); + } + + + /* LDC AND NUMBER TYPE HELPERS */ + + /** + * Signifies the type of number constant for a {@link NumberType}. + */ + public enum NumberType { + INTEGER(Number::intValue), + FLOAT(Number::floatValue), + LONG(Number::longValue), + DOUBLE(Number::doubleValue); + + private final Function mapper; + + NumberType(Function mapper) { + this.mapper = mapper; + } + } + + /** + * Casts a given number to a given specific {@link NumberType}. This helps alleviate the problems that comes with + * JavaScript's ambiguous number system. + *

+ * The result is returned as an {@link Object} so it can be used as a value in various instructions that require + * values, such as {@link FieldNode} and {@link LdcInsnNode}. + * + * @param value The number to cast + * @param type The type of number to cast to + * @return The casted number + */ + public static Object castNumber(final Number value, final NumberType type) { + return type.mapper.apply(value); + } + + /** + * Builds a new {@link LdcInsnNode} with the given number value and {@link NumberType}. + * + * @param value The number value + * @param type The type of the number + * @return The built LDC node + */ + public static LdcInsnNode buildNumberLdcInsnNode(final Number value, final NumberType type) { + return new LdcInsnNode(castNumber(value, type)); + } + + + /* SPECIALIZED TRANSFORMATION */ + /** * Rewrites accesses to a specific field in the given class to a method-call. *

@@ -663,8 +864,10 @@ public static InsnList listOf(AbstractInsnNode... nodes) { * * @param classNode the class to rewrite the accesses in * @param fieldName the field accesses should be redirected to - * @param methodName the name of the method to redirect accesses through, or null if any method with matching - * signature should be applicable + * @param methodName the name of the method to redirect accesses through, or {@code null} if any method with + * matching signature should be applicable + * @apiNote This method was written as a special use case for Forge. It is not recommended to use this method + * unless you know what you are doing. */ public static void redirectFieldToMethod(final ClassNode classNode, final String fieldName, @Nullable final String methodName) { MethodNode foundMethod = null; @@ -707,31 +910,87 @@ public static void redirectFieldToMethod(final ClassNode classNode, final String for (MethodNode methodNode : classNode.methods) { // skip the found getter method if (methodNode == foundMethod) continue; - if (!Objects.equals(methodNode.desc, methodSignature)) { - final ListIterator iterator = methodNode.instructions.iterator(); - while (iterator.hasNext()) { - AbstractInsnNode insnNode = iterator.next(); - if (insnNode.getOpcode() == Opcodes.GETFIELD) { - FieldInsnNode fieldInsnNode = (FieldInsnNode) insnNode; - if (Objects.equals(fieldInsnNode.name, fieldName)) { - iterator.remove(); - MethodInsnNode replace = new MethodInsnNode(Opcodes.INVOKEVIRTUAL, classNode.name, foundMethod.name, foundMethod.desc, false); - iterator.add(replace); - } + + if (Objects.equals(methodNode.desc, methodSignature)) continue; + + final ListIterator iterator = methodNode.instructions.iterator(); + while (iterator.hasNext()) { + AbstractInsnNode insnNode = iterator.next(); + if (insnNode.getOpcode() == Opcodes.GETFIELD) { + FieldInsnNode fieldInsnNode = (FieldInsnNode) insnNode; + if (Objects.equals(fieldInsnNode.name, fieldName)) { + iterator.remove(); + MethodInsnNode replace = new MethodInsnNode(Opcodes.INVOKEVIRTUAL, classNode.name, foundMethod.name, foundMethod.desc, false); + iterator.add(replace); } } } } } + + /* SRG NAME REMAPPING */ + + /** + * Maps a method from the given SRG name to the mapped name at deobfuscated runtime. + * + * @param name The SRG name of the method + * @return The mapped name of the method + * + * @apiNote As of Minecraft 1.20.4, Forge no longer uses SRG names in production. While the mapping system will + * still work for sake of backwards-compatibility, you should not be using this method if you are on 1.20.4 or + * later. + */ + public static String mapMethod(String name) { + return map(name, INameMappingService.Domain.METHOD); + } + + /** + * Maps a field from the given SRG name to the mapped name at deobfuscated runtime. + * + * @param name The SRG name of the field + * @return The mapped name of the field + * + * @apiNote As of Minecraft 1.20.4, Forge no longer uses SRG names in production. While the mapping system will + * still work for sake of backwards-compatibility, you should not be using this method if you are on 1.20.4 or + * later. + */ + public static String mapField(String name) { + return map(name, INameMappingService.Domain.FIELD); + } + + private static String map(String name, INameMappingService.Domain domain) { + return Optional.ofNullable(Launcher.INSTANCE). + map(Launcher::environment). + flatMap(env -> env.findNameMapping("srg")). + map(f -> f.apply(domain, name)).orElse(name); + } + + + /* ADDITIONAL DATA */ + + /** + * Checks if the given JVM property (or if the property prepended with {@code "coremod."}) is {@code true}. + * + * @param propertyName the property to check + * @return true if the property is {@code true} + */ + public static boolean getSystemPropertyFlag(final String propertyName) { + // TODO: Remove all backwards-compatible logic in 6.0 + return Boolean.getBoolean(propertyName) // actually checks the flag + || Boolean.getBoolean("coremod." + propertyName) // the original intended purpose + || Boolean.getBoolean(System.getProperty("coremod." + propertyName, "TRUE")); // the bugged logic for backwards-compatibility + } + /** * Loads a JavaScript file by file name. Useful for reusing code across multiple files. * - * @param file The file name to load. - * @return true if file load was successful. + * @param file The file name to load + * @return {@code true} if the file load was successful. The file will only be loaded in the + * {@code initializeCoreMod()} or any of the transformer functions returned by it. * - * @throws ScriptException If the script engine encounters an error, usually due to a syntax error in the script. - * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file. + * @throws ScriptException If the script engine encounters an error, usually due to a syntax error in the script + * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file */ public static boolean loadFile(String file) throws ScriptException, IOException { return CoreModTracker.loadFileByName(file); @@ -740,24 +999,29 @@ public static boolean loadFile(String file) throws ScriptException, IOException /** * Loads JSON data from a file by file name. * - * @param file The file name to load. - * @return The loaded JSON data if successful, or null if not. + * @param file The file name to load + * @return The loaded JSON data if successful, or {@code null} if not. The data will only be loaded in the + * {@code initializeCoreMod()} or any of the transformer functions returned by it. * - * @throws ScriptException If the parsed JSON data is malformed. - * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file. + * @throws ScriptException If the parsed JSON data is malformed + * @throws IOException If an I/O error occurs while reading the file, usually due to a corrupt or missing file */ @Nullable public static Object loadData(String file) throws ScriptException, IOException { return CoreModTracker.loadDataByName(file); } + + /* LOGGING AND DEBUGGING */ + /** * Logs the given message at the given level. The message can contain formatting arguments. Uses a * {@link org.apache.logging.log4j.Logger}. * - * @param level Log level - * @param message Message - * @param args Formatting arguments + * @param level The log level + * @param message The message + * @param args Any formatting arguments + * @see CoreModTracker#log(String, String, Object[]) */ public static void log(String level, String message, Object... args) { CoreModTracker.log(level, message, args); @@ -766,8 +1030,8 @@ public static void log(String level, String message, Object... args) { /** * Converts a {@link ClassNode} to a string representation. Useful for evaluating changes after transformation. * - * @param node The class node to convert. - * @return The string representation of the class node. + * @param node The class node to convert + * @return The string representation of the class node */ public static String classNodeToString(ClassNode node) { Textifier text = new Textifier(); @@ -776,59 +1040,59 @@ public static String classNodeToString(ClassNode node) { } /** - * Converts a {@link FieldNode} to a string representation. Useful for evaluating changes after transformation. + * Converts a {@link MethodNode} to a string representation. Useful for evaluating changes after transformation. * - * @param node The field node to convert. - * @return The string representation of the field node. + * @param node The method node to convert + * @return The string representation of the method node */ - public static String fieldNodeToString(FieldNode node) { + public static String methodNodeToString(MethodNode node) { Textifier text = new Textifier(); - node.accept(new TraceClassVisitor(null, text, null)); + node.accept(new TraceMethodVisitor(text)); return toString(text); } /** - * Converts a {@link MethodNode} to a string representation. Useful for evaluating changes after transformation. + * Converts a {@link FieldNode} to a string representation. Useful for evaluating changes after transformation. * - * @param node The method node to convert. - * @return The string representation of the method node. + * @param node The field node to convert + * @return The string representation of the field node */ - public static String methodNodeToString(MethodNode node) { + public static String fieldNodeToString(FieldNode node) { Textifier text = new Textifier(); - node.accept(new TraceMethodVisitor(text)); + node.accept(new TraceClassVisitor(null, text, null)); return toString(text); } /** - * Converts an {@link InsnNode} to a string representation. + * Converts an {@link InsnList} to a string representation, displaying each instruction in the list similar to + * {@link #insnToString(AbstractInsnNode)}. * - * @param insn The instruction to convert. - * @return The string representation of the instruction. + * @param list The list to convert + * @return The string representation of the instruction list */ - public static String insnToString(AbstractInsnNode insn) { + public static String insnListToString(InsnList list) { Textifier text = new Textifier(); - insn.accept(new TraceMethodVisitor(text)); + list.accept(new TraceMethodVisitor(text)); return toString(text); } /** - * Converts a {@link InsnList} to a string representation, displaying each instruction in the list similar to - * {@link #insnToString(AbstractInsnNode)}. + * Converts an {@link AbstractInsnNode} to a string representation. * - * @param list The list to convert. - * @return The string + * @param insn The instruction to convert + * @return The string representation of the instruction */ - public static String insnListToString(InsnList list) { + public static String insnToString(AbstractInsnNode insn) { Textifier text = new Textifier(); - list.accept(new TraceMethodVisitor(text)); + insn.accept(new TraceMethodVisitor(text)); return toString(text); } /** * Gets the LDC constant's class name as a string. Useful for debugging existing LDC instructions. * - * @param insn The LDC instruction. - * @return The class name of the LDC constant. + * @param insn The LDC instruction + * @return The class name of the LDC constant */ public static String ldcInsnClassToString(LdcInsnNode insn) { return insn.cst.getClass().toString(); diff --git a/src/main/java/net/minecraftforge/coremod/transformer/CoreModBaseTransformer.java b/src/main/java/net/minecraftforge/coremod/transformer/CoreModBaseTransformer.java index b0b61af..ae9aa6f 100644 --- a/src/main/java/net/minecraftforge/coremod/transformer/CoreModBaseTransformer.java +++ b/src/main/java/net/minecraftforge/coremod/transformer/CoreModBaseTransformer.java @@ -13,11 +13,15 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; -import org.jetbrains.annotations.NotNull; import java.util.Set; import java.util.function.Function; +/** + * The base-level transformer for CoreMods. + * + * @param The type of node to transform + */ public abstract class CoreModBaseTransformer implements ITransformer { static final Logger LOGGER = LogManager.getLogger(); static final Marker COREMOD = MarkerManager.getMarker("COREMOD"); @@ -26,6 +30,14 @@ public abstract class CoreModBaseTransformer implements ITransformer { final Function function; final String coreName; + /** + * Creates a new base-level transformer with the given targets and transformer function. + * + * @param coreMod The CoreMod that this transformer belongs to + * @param coreName The name of the CoreMod + * @param targets The targets to apply this transformer to + * @param function The transformer function + */ public CoreModBaseTransformer(CoreMod coreMod, final String coreName, final Set targets, final Function function) { this.coreMod = coreMod; this.coreName = coreName; @@ -33,7 +45,13 @@ public CoreModBaseTransformer(CoreMod coreMod, final String coreName, final Set< this.function = function; } - @NotNull + /** + * Transforms the given input node using the transformer function. + * + * @param input The ASM input node to transform + * @param context The voting context for ModLauncher + * @return The transformed node + */ @Override public T transform(T input, ITransformerVotingContext context) { CoreModTracker.setCoreMod(coreMod); @@ -42,6 +60,7 @@ public T transform(T input, ITransformerVotingContext context) { result = runCoremod(result); } catch (Exception e) { LOGGER.error(COREMOD, "Error occurred applying transform of coremod {} function {}", this.coreMod.getPath(), this.coreName, e); + // TODO CRASH THE FUCKING GAME HERE } finally { CoreModTracker.clearCoreMod(); } @@ -50,20 +69,37 @@ public T transform(T input, ITransformerVotingContext context) { abstract T runCoremod(T input); - @NotNull + /** + * The transformer vote that this CoreMod should use as a result of transformation. + * + * @param context The context of the vote + * @return The desired transformer vote + */ @Override public TransformerVoteResult castVote(ITransformerVotingContext context) { return TransformerVoteResult.YES; } - @NotNull + /** + * Gets the desired transformer targets of this CoreMod. + * + * @return The targets for transformation + * + * @apiNote The result of this method does not usually indicate that it will be used directly in + * @link #runCoremod(Object)}. + */ @Override public Set targets() { return targets; } + /** + * Gets the identification labels of this transformer to be used in ModLauncher. + * + * @return The identification labels + */ @Override public String[] labels() { - return new String[] { coreMod.getFile().getOwnerId(), coreName }; + return new String[] {coreMod.getFile().getOwnerId(), coreName}; } }