mirror of
https://github.com/Minestom/Minestom.git
synced 2024-10-01 07:57:41 +02:00
Merge branch 'mixin-support'
This commit is contained in:
commit
6df42b79cb
13
build.gradle
13
build.gradle
@ -30,6 +30,10 @@ allprojects {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url 'https://libraries.minecraft.net' }
|
maven { url 'https://libraries.minecraft.net' }
|
||||||
maven { url 'https://jitpack.io' }
|
maven { url 'https://jitpack.io' }
|
||||||
|
maven {
|
||||||
|
name 'sponge'
|
||||||
|
url 'http://repo.spongepowered.org/maven'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +113,15 @@ dependencies {
|
|||||||
api 'org.projectlombok:lombok:1.18.12'
|
api 'org.projectlombok:lombok:1.18.12'
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.12'
|
annotationProcessor 'org.projectlombok:lombok:1.18.12'
|
||||||
|
|
||||||
|
// Code modification
|
||||||
|
api "org.ow2.asm:asm:${asmVersion}"
|
||||||
|
api "org.ow2.asm:asm-tree:${asmVersion}"
|
||||||
|
api "org.ow2.asm:asm-analysis:${asmVersion}"
|
||||||
|
api "org.ow2.asm:asm-util:${asmVersion}"
|
||||||
|
api "org.ow2.asm:asm-commons:${asmVersion}"
|
||||||
|
implementation 'com.google.guava:guava:21.0'
|
||||||
|
api "org.spongepowered:mixin:${mixinVersion}"
|
||||||
|
|
||||||
// Path finding
|
// Path finding
|
||||||
api 'com.github.MadMartian:hydrazine-path-finding:1.4.2'
|
api 'com.github.MadMartian:hydrazine-path-finding:1.4.2'
|
||||||
|
|
||||||
|
2
gradle.properties
Normal file
2
gradle.properties
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
asmVersion=8.0.1
|
||||||
|
mixinVersion=0.8
|
@ -36,6 +36,7 @@ public class Main {
|
|||||||
commandManager.register(new ShutdownCommand());
|
commandManager.register(new ShutdownCommand());
|
||||||
commandManager.register(new TeleportCommand());
|
commandManager.register(new TeleportCommand());
|
||||||
|
|
||||||
|
|
||||||
StorageManager storageManager = MinecraftServer.getStorageManager();
|
StorageManager storageManager = MinecraftServer.getStorageManager();
|
||||||
storageManager.defineDefaultStorageSystem(FileStorageSystem::new);
|
storageManager.defineDefaultStorageSystem(FileStorageSystem::new);
|
||||||
|
|
||||||
|
57
src/main/java/net/minestom/server/Bootstrap.java
Normal file
57
src/main/java/net/minestom/server/Bootstrap.java
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package net.minestom.server;
|
||||||
|
|
||||||
|
import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader;
|
||||||
|
import net.minestom.server.extras.selfmodification.mixins.MixinCodeModifier;
|
||||||
|
import net.minestom.server.extras.selfmodification.mixins.MixinServiceMinestom;
|
||||||
|
import org.spongepowered.asm.launch.MixinBootstrap;
|
||||||
|
import org.spongepowered.asm.launch.platform.CommandLineOptions;
|
||||||
|
import org.spongepowered.asm.mixin.Mixins;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to launch Minestom with the {@link MinestomOverwriteClassLoader} to allow for self-modifications
|
||||||
|
*/
|
||||||
|
public final class Bootstrap {
|
||||||
|
|
||||||
|
public static void bootstrap(String mainClassFullName, String[] args) {
|
||||||
|
try {
|
||||||
|
ClassLoader classLoader = MinestomOverwriteClassLoader.getInstance();
|
||||||
|
startMixin(args);
|
||||||
|
MinestomOverwriteClassLoader.getInstance().addCodeModifier(new MixinCodeModifier());
|
||||||
|
|
||||||
|
MixinServiceMinestom.gotoPreinitPhase();
|
||||||
|
// ensure extensions are loaded when starting the server
|
||||||
|
Class<?> serverClass = classLoader.loadClass("net.minestom.server.MinecraftServer");
|
||||||
|
Method init = serverClass.getMethod("init");
|
||||||
|
init.invoke(null);
|
||||||
|
MixinServiceMinestom.gotoInitPhase();
|
||||||
|
|
||||||
|
MixinServiceMinestom.gotoDefaultPhase();
|
||||||
|
|
||||||
|
Class<?> mainClass = classLoader.loadClass(mainClassFullName);
|
||||||
|
Method main = mainClass.getDeclaredMethod("main", String[].class);
|
||||||
|
main.invoke(null, new Object[] { args });
|
||||||
|
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void startMixin(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
|
||||||
|
// hacks required to pass custom arguments
|
||||||
|
Method start = MixinBootstrap.class.getDeclaredMethod("start");
|
||||||
|
start.setAccessible(true);
|
||||||
|
if (! ((boolean)start.invoke(null)) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Method doInit = MixinBootstrap.class.getDeclaredMethod("doInit", CommandLineOptions.class);
|
||||||
|
doInit.setAccessible(true);
|
||||||
|
doInit.invoke(null, CommandLineOptions.ofArgs(Arrays.asList(args)));
|
||||||
|
|
||||||
|
MixinBootstrap.getPlatform().inject();
|
||||||
|
Mixins.getConfigs().forEach(c -> MinestomOverwriteClassLoader.getInstance().protectedPackages.add(c.getConfig().getMixinPackage()));
|
||||||
|
}
|
||||||
|
}
|
@ -147,6 +147,9 @@ public class MinecraftServer {
|
|||||||
public static MinecraftServer init() {
|
public static MinecraftServer init() {
|
||||||
if (minecraftServer != null) // don't init twice
|
if (minecraftServer != null) // don't init twice
|
||||||
return minecraftServer;
|
return minecraftServer;
|
||||||
|
extensionManager = new ExtensionManager();
|
||||||
|
extensionManager.loadExtensions();
|
||||||
|
|
||||||
// warmup/force-init registries
|
// warmup/force-init registries
|
||||||
// without this line, registry types that are not loaded explicitly will have an internal empty registry in Registries
|
// without this line, registry types that are not loaded explicitly will have an internal empty registry in Registries
|
||||||
// That can happen with PotionType for instance, if no code tries to access a PotionType field
|
// That can happen with PotionType for instance, if no code tries to access a PotionType field
|
||||||
@ -182,8 +185,6 @@ public class MinecraftServer {
|
|||||||
|
|
||||||
updateManager = new UpdateManager();
|
updateManager = new UpdateManager();
|
||||||
|
|
||||||
extensionManager = new ExtensionManager();
|
|
||||||
|
|
||||||
lootTableManager = new LootTableManager();
|
lootTableManager = new LootTableManager();
|
||||||
tagManager = new TagManager();
|
tagManager = new TagManager();
|
||||||
|
|
||||||
@ -454,7 +455,6 @@ public class MinecraftServer {
|
|||||||
updateManager.start();
|
updateManager.start();
|
||||||
nettyServer.start(address, port);
|
nettyServer.start(address, port);
|
||||||
long t1 = -System.nanoTime();
|
long t1 = -System.nanoTime();
|
||||||
extensionManager.loadExtensionJARs();
|
|
||||||
// Init extensions
|
// Init extensions
|
||||||
// TODO: Extensions should handle depending on each other and have a load-order.
|
// TODO: Extensions should handle depending on each other and have a load-order.
|
||||||
extensionManager.getExtensions().forEach(Extension::preInitialize);
|
extensionManager.getExtensions().forEach(Extension::preInitialize);
|
||||||
|
@ -1,36 +1,35 @@
|
|||||||
package net.minestom.server.extensions;
|
package net.minestom.server.extensions;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.*;
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.spongepowered.asm.mixin.Mixins;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.*;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLClassLoader;
|
import java.net.URLClassLoader;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
import java.util.zip.ZipFile;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ExtensionManager {
|
public class ExtensionManager {
|
||||||
private final Map<URL, URLClassLoader> extensionLoaders = new HashMap<>();
|
private final Map<String, URLClassLoader> extensionLoaders = new HashMap<>();
|
||||||
private final Map<String, Extension> extensions = new HashMap<>();
|
private final Map<String, Extension> extensions = new HashMap<>();
|
||||||
private final File extensionFolder = new File("extensions");
|
private final File extensionFolder = new File("extensions");
|
||||||
|
private final static String INDEV_CLASSES_FOLDER = "minestom.extension.indevfolder.classes";
|
||||||
|
private final static String INDEV_RESOURCES_FOLDER = "minestom.extension.indevfolder.resources";
|
||||||
|
|
||||||
public ExtensionManager() {
|
public ExtensionManager() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loadExtensionJARs() {
|
public void loadExtensions() {
|
||||||
if (!extensionFolder.exists()) {
|
if (!extensionFolder.exists()) {
|
||||||
if (!extensionFolder.mkdirs()) {
|
if (!extensionFolder.mkdirs()) {
|
||||||
log.error("Could not find or create the extension folder, extensions will not be loaded!");
|
log.error("Could not find or create the extension folder, extensions will not be loaded!");
|
||||||
@ -38,25 +37,32 @@ public class ExtensionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (File file : extensionFolder.listFiles()) {
|
List<DiscoveredExtension> discoveredExtensions = discoverExtensions();
|
||||||
if (file.isDirectory()) {
|
setupCodeModifiers(discoveredExtensions);
|
||||||
continue;
|
|
||||||
}
|
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
|
||||||
if (!file.getName().endsWith(".jar")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
URLClassLoader loader;
|
URLClassLoader loader;
|
||||||
|
URL[] urls = new URL[discoveredExtension.files.length];
|
||||||
try {
|
try {
|
||||||
URL url = file.toURI().toURL();
|
for (int i = 0; i < urls.length; i++) {
|
||||||
loader = loadJar(url);
|
urls[i] = discoveredExtension.files[i].toURI().toURL();
|
||||||
extensionLoaders.put(url, loader);
|
}
|
||||||
|
loader = newClassLoader(urls);
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
log.error(String.format("Failed to get URL for file %s.", file.getPath()));
|
log.error("Failed to get URL.", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
InputStream extensionInputStream = loader.getResourceAsStream("extension.json");
|
InputStream extensionInputStream = loader.getResourceAsStream("extension.json");
|
||||||
if (extensionInputStream == null) {
|
if (extensionInputStream == null) {
|
||||||
log.error(String.format("Failed to find extension.json in the file '%s'.", file.getPath()));
|
StringBuilder urlsString = new StringBuilder();
|
||||||
|
for (int i = 0; i < urls.length; i++) {
|
||||||
|
URL url = urls[i];
|
||||||
|
if(i != 0) {
|
||||||
|
urlsString.append(" ; ");
|
||||||
|
}
|
||||||
|
urlsString.append("'").append(url.toString()).append("'");
|
||||||
|
}
|
||||||
|
log.error(String.format("Failed to find extension.json in the urls '%s'.", urlsString));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
JsonObject extensionDescription = JsonParser.parseReader(new InputStreamReader(extensionInputStream)).getAsJsonObject();
|
JsonObject extensionDescription = JsonParser.parseReader(new InputStreamReader(extensionInputStream)).getAsJsonObject();
|
||||||
@ -64,6 +70,8 @@ public class ExtensionManager {
|
|||||||
String mainClass = extensionDescription.get("entrypoint").getAsString();
|
String mainClass = extensionDescription.get("entrypoint").getAsString();
|
||||||
String extensionName = extensionDescription.get("name").getAsString();
|
String extensionName = extensionDescription.get("name").getAsString();
|
||||||
|
|
||||||
|
extensionLoaders.put(extensionName, loader);
|
||||||
|
|
||||||
if (extensions.containsKey(extensionName.toLowerCase())) {
|
if (extensions.containsKey(extensionName.toLowerCase())) {
|
||||||
log.error(String.format("An extension called '%s' has already been registered.", extensionName));
|
log.error(String.format("An extension called '%s' has already been registered.", extensionName));
|
||||||
return;
|
return;
|
||||||
@ -137,14 +145,53 @@ public class ExtensionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<DiscoveredExtension> discoverExtensions() {
|
||||||
|
Gson gson = new Gson();
|
||||||
|
List<DiscoveredExtension> extensions = new LinkedList<>();
|
||||||
|
for (File file : extensionFolder.listFiles()) {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!file.getName().endsWith(".jar")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try(ZipFile f = new ZipFile(file);
|
||||||
|
InputStreamReader reader = new InputStreamReader(f.getInputStream(f.getEntry("extension.json")))) {
|
||||||
|
|
||||||
|
DiscoveredExtension extension = new DiscoveredExtension();
|
||||||
|
extension.files = new File[]{file};
|
||||||
|
extension.description = gson.fromJson(reader, JsonObject.class);
|
||||||
|
extensions.add(extension);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this allows developers to have their extension discovered while working on it, without having to build a jar and put in the extension folder
|
||||||
|
if(System.getProperty(INDEV_CLASSES_FOLDER) != null && System.getProperty(INDEV_RESOURCES_FOLDER) != null) {
|
||||||
|
log.info("Found indev folders for extension. Adding to list of discovered extensions.");
|
||||||
|
String extensionClasses = System.getProperty(INDEV_CLASSES_FOLDER);
|
||||||
|
String extensionResources = System.getProperty(INDEV_RESOURCES_FOLDER);
|
||||||
|
try(InputStreamReader reader = new InputStreamReader(new FileInputStream(new File(extensionResources, "extension.json")))) {
|
||||||
|
DiscoveredExtension extension = new DiscoveredExtension();
|
||||||
|
extension.files = new File[] { new File(extensionClasses), new File(extensionResources) };
|
||||||
|
extension.description = gson.fromJson(reader, JsonObject.class);
|
||||||
|
extensions.add(extension);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extensions;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a URL into the classpath.
|
* Loads a URL into the classpath.
|
||||||
*
|
*
|
||||||
* @param url {@link URL} (usually a JAR) that should be loaded.
|
* @param urls {@link URL} (usually a JAR) that should be loaded.
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
public URLClassLoader loadJar(@NotNull URL url) {
|
public URLClassLoader newClassLoader(@NotNull URL[] urls) {
|
||||||
return URLClassLoader.newInstance(new URL[]{url}, ExtensionManager.class.getClassLoader());
|
return URLClassLoader.newInstance(urls, ExtensionManager.class.getClassLoader());
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@ -163,7 +210,44 @@ public class ExtensionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public Map<URL, URLClassLoader> getExtensionLoaders() {
|
public Map<String, URLClassLoader> getExtensionLoaders() {
|
||||||
return new HashMap<>(extensionLoaders);
|
return new HashMap<>(extensionLoaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extensions are allowed to apply Mixin transformers, the magic happens here
|
||||||
|
*/
|
||||||
|
private void setupCodeModifiers(List<DiscoveredExtension> extensions) {
|
||||||
|
ClassLoader cl = getClass().getClassLoader();
|
||||||
|
if(!(cl instanceof MinestomOverwriteClassLoader)) {
|
||||||
|
log.warn("Current class loader is not a MinestomOverwriteClassLoader, but "+cl+". This disables code modifiers (Mixin support is therefore disabled)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MinestomOverwriteClassLoader modifiableClassLoader = (MinestomOverwriteClassLoader)cl;
|
||||||
|
log.info("Start loading code modifiers...");
|
||||||
|
for(DiscoveredExtension extension : extensions) {
|
||||||
|
try {
|
||||||
|
if(extension.description.has("codeModifiers")) {
|
||||||
|
JsonArray codeModifierClasses = extension.description.getAsJsonArray("codeModifiers");
|
||||||
|
for(JsonElement elem : codeModifierClasses) {
|
||||||
|
modifiableClassLoader.loadModifier(extension.files, elem.getAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(extension.description.has("mixinConfig")) {
|
||||||
|
String mixinConfigFile = extension.description.get("mixinConfig").getAsString();
|
||||||
|
Mixins.addConfiguration(mixinConfigFile);
|
||||||
|
log.info("Found mixin in extension "+extension.description.get("name").getAsString()+": "+mixinConfigFile);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
log.error("Failed to load code modifier for extension in files: "+Arrays.toString(extension.files), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("Done loading code modifiers.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DiscoveredExtension {
|
||||||
|
private File[] files;
|
||||||
|
private JsonObject description;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package net.minestom.server.extras.selfmodification;
|
||||||
|
|
||||||
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will be called by {@link MinestomOverwriteClassLoader} to transform classes at load-time
|
||||||
|
*/
|
||||||
|
public abstract class CodeModifier {
|
||||||
|
/**
|
||||||
|
* Must return true iif the class node has been modified
|
||||||
|
* @param source
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public abstract boolean transform(ClassNode source);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beginning of the class names to transform.
|
||||||
|
* 'null' is allowed to transform any class, but not recommended
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public abstract String getNamespace();
|
||||||
|
}
|
@ -0,0 +1,228 @@
|
|||||||
|
package net.minestom.server.extras.selfmodification;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.ClassWriter;
|
||||||
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Loader that can modify class bytecode when they are loaded
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class MinestomOverwriteClassLoader extends URLClassLoader {
|
||||||
|
|
||||||
|
private static MinestomOverwriteClassLoader INSTANCE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classes that cannot be loaded/modified by this classloader.
|
||||||
|
* Will go through parent class loader
|
||||||
|
*/
|
||||||
|
public final Set<String> protectedClasses = new HashSet<>() {
|
||||||
|
{
|
||||||
|
add("net.minestom.server.extras.selfmodification.CodeModifier");
|
||||||
|
add("net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
public final Set<String> protectedPackages = new HashSet<>() {
|
||||||
|
{
|
||||||
|
add("com.google");
|
||||||
|
add("com.mojang");
|
||||||
|
add("org.objectweb.asm");
|
||||||
|
add("org.slf4j");
|
||||||
|
add("org.apache");
|
||||||
|
add("org.spongepowered");
|
||||||
|
add("net.minestom.server.extras.selfmodification");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Used to let ASM find out common super types, without actually commiting to loading them
|
||||||
|
* Otherwise ASM would accidentally load classes we might want to modify
|
||||||
|
*/
|
||||||
|
private final URLClassLoader asmClassLoader;
|
||||||
|
|
||||||
|
// TODO: replace by tree to optimize lookup times. We can use the fact that package names are split with '.' to allow for fast lookup
|
||||||
|
// TODO: eg. Node("java", Node("lang"), Node("io")). Loading "java.nio.Channel" would apply modifiers from "java", but not "java.io" or "java.lang".
|
||||||
|
// TODO: that's an example, please don't modify standard library classes. And this classloader should not let you do it because it first asks the platform classloader
|
||||||
|
|
||||||
|
// TODO: priorities?
|
||||||
|
private List<CodeModifier> modifiers = new LinkedList<>();
|
||||||
|
|
||||||
|
private MinestomOverwriteClassLoader(ClassLoader parent) {
|
||||||
|
super("Minestom ClassLoader", extractURLsFromClasspath(), parent);
|
||||||
|
asmClassLoader = newChild(new URL[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MinestomOverwriteClassLoader getInstance() {
|
||||||
|
if(INSTANCE == null) {
|
||||||
|
synchronized (MinestomOverwriteClassLoader.class) {
|
||||||
|
if(INSTANCE == null) {
|
||||||
|
INSTANCE = new MinestomOverwriteClassLoader(MinestomOverwriteClassLoader.class.getClassLoader());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static URL[] extractURLsFromClasspath() {
|
||||||
|
String classpath = System.getProperty("java.class.path");
|
||||||
|
String[] parts = classpath.split(";");
|
||||||
|
URL[] urls = new URL[parts.length];
|
||||||
|
for (int i = 0; i < urls.length; i++) {
|
||||||
|
try {
|
||||||
|
String part = parts[i];
|
||||||
|
String protocol;
|
||||||
|
if(part.contains("!")) {
|
||||||
|
protocol = "jar://";
|
||||||
|
} else {
|
||||||
|
protocol = "file://";
|
||||||
|
}
|
||||||
|
urls[i] = new URL(protocol+part);
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new Error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||||
|
Class<?> loadedClass = findLoadedClass(name);
|
||||||
|
if(loadedClass != null)
|
||||||
|
return loadedClass;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// we do not load system classes by ourselves
|
||||||
|
Class<?> systemClass = ClassLoader.getPlatformClassLoader().loadClass(name);
|
||||||
|
log.trace("System class: "+systemClass);
|
||||||
|
return systemClass;
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
try {
|
||||||
|
if(isProtected(name)) {
|
||||||
|
log.trace("Protected: "+name);
|
||||||
|
return super.loadClass(name, resolve);
|
||||||
|
}
|
||||||
|
|
||||||
|
return define(name, loadBytes(name, true), resolve);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.trace("Fail to load class, resorting to parent loader: "+name, ex);
|
||||||
|
// fail to load class, let parent load
|
||||||
|
// this forbids code modification, but at least it will load
|
||||||
|
return super.loadClass(name, resolve);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isProtected(String name) {
|
||||||
|
if(!protectedClasses.contains(name)) {
|
||||||
|
for(String start : protectedPackages) {
|
||||||
|
if(name.startsWith(start))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Class<?> define(String name, byte[] bytes, boolean resolve) throws ClassNotFoundException {
|
||||||
|
Class<?> defined = defineClass(name, bytes, 0, bytes.length);
|
||||||
|
log.trace("Loaded with code modifiers: "+name);
|
||||||
|
if(resolve) {
|
||||||
|
resolveClass(defined);
|
||||||
|
}
|
||||||
|
return defined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and possibly transforms class bytecode corresponding to the given binary name.
|
||||||
|
* @param name
|
||||||
|
* @param transform
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
* @throws ClassNotFoundException
|
||||||
|
*/
|
||||||
|
public byte[] loadBytes(String name, boolean transform) throws IOException, ClassNotFoundException {
|
||||||
|
if(name == null)
|
||||||
|
throw new ClassNotFoundException();
|
||||||
|
String path = name.replace(".", "/") + ".class";
|
||||||
|
byte[] bytes = getResourceAsStream(path).readAllBytes();
|
||||||
|
if(transform && !isProtected(name)) {
|
||||||
|
ClassReader reader = new ClassReader(bytes);
|
||||||
|
ClassNode node = new ClassNode();
|
||||||
|
reader.accept(node, 0);
|
||||||
|
boolean modified = false;
|
||||||
|
synchronized (modifiers) {
|
||||||
|
for(CodeModifier modifier : modifiers) {
|
||||||
|
boolean shouldModify = modifier.getNamespace() == null || name.startsWith(modifier.getNamespace());
|
||||||
|
if(shouldModify) {
|
||||||
|
modified |= modifier.transform(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(modified) {
|
||||||
|
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES) {
|
||||||
|
@Override
|
||||||
|
protected ClassLoader getClassLoader() {
|
||||||
|
return asmClassLoader;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
node.accept(writer);
|
||||||
|
bytes = writer.toByteArray();
|
||||||
|
log.trace("Modified "+name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// overriden to increase access (from protected to public)
|
||||||
|
@Override
|
||||||
|
public Class<?> findClass(String name) throws ClassNotFoundException {
|
||||||
|
return super.findClass(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public URLClassLoader newChild(@NotNull URL[] urls) {
|
||||||
|
return URLClassLoader.newInstance(urls, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadModifier(File[] originFiles, String codeModifierClass) {
|
||||||
|
URL[] urls = new URL[originFiles.length];
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < originFiles.length; i++) {
|
||||||
|
urls[i] = originFiles[i].toURI().toURL();
|
||||||
|
}
|
||||||
|
URLClassLoader loader = newChild(urls);
|
||||||
|
Class<?> modifierClass = loader.loadClass(codeModifierClass);
|
||||||
|
if(CodeModifier.class.isAssignableFrom(modifierClass)) {
|
||||||
|
CodeModifier modifier = (CodeModifier) modifierClass.getDeclaredConstructor().newInstance();
|
||||||
|
synchronized (modifiers) {
|
||||||
|
log.warn("Added Code modifier: "+modifier);
|
||||||
|
addCodeModifier(modifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (MalformedURLException | ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCodeModifier(CodeModifier modifier) {
|
||||||
|
synchronized (modifiers) {
|
||||||
|
modifiers.add(modifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CodeModifier> getModifiers() {
|
||||||
|
return modifiers;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package net.minestom.server.extras.selfmodification.mixins;
|
||||||
|
|
||||||
|
import org.spongepowered.asm.service.IGlobalPropertyService;
|
||||||
|
import org.spongepowered.asm.service.IPropertyKey;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global properties service for Mixin
|
||||||
|
*/
|
||||||
|
public class GlobalPropertyServiceMinestom implements IGlobalPropertyService {
|
||||||
|
|
||||||
|
private class BasicProperty implements IPropertyKey {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
public BasicProperty(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
BasicProperty that = (BasicProperty) o;
|
||||||
|
return Objects.equals(name, that.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "BasicProperty{" +
|
||||||
|
"name='" + name + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<String, IPropertyKey> keys = new HashMap<>();
|
||||||
|
private final Map<IPropertyKey, Object> values = new HashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IPropertyKey resolveKey(String name) {
|
||||||
|
return keys.computeIfAbsent(name, k -> new BasicProperty(k));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getProperty(IPropertyKey key) {
|
||||||
|
return (T) values.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setProperty(IPropertyKey key, Object value) {
|
||||||
|
values.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getProperty(IPropertyKey key, T defaultValue) {
|
||||||
|
return (T) values.getOrDefault(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPropertyString(IPropertyKey key, String defaultValue) {
|
||||||
|
return (String) values.getOrDefault(key, defaultValue);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package net.minestom.server.extras.selfmodification.mixins;
|
||||||
|
|
||||||
|
import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader;
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
|
import org.spongepowered.asm.service.IClassBytecodeProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides class bytecode for Mixin
|
||||||
|
*/
|
||||||
|
public class MinestomBytecodeProvider implements IClassBytecodeProvider {
|
||||||
|
private final MinestomOverwriteClassLoader classLoader;
|
||||||
|
|
||||||
|
public MinestomBytecodeProvider(MinestomOverwriteClassLoader classLoader) {
|
||||||
|
this.classLoader = classLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassNode getClassNode(String name) throws ClassNotFoundException, IOException {
|
||||||
|
return getClassNode(name, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClassNode loadNode(String name, boolean transform) throws ClassNotFoundException {
|
||||||
|
ClassNode node = new ClassNode();
|
||||||
|
ClassReader reader;
|
||||||
|
try {
|
||||||
|
reader = new ClassReader(classLoader.loadBytes(name, transform));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ClassNotFoundException("Could not load ClassNode with name "+name, e);
|
||||||
|
}
|
||||||
|
reader.accept(node, 0);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClassNode getClassNode(String name, boolean runTransformers) throws ClassNotFoundException, IOException {
|
||||||
|
return loadNode(name, runTransformers);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package net.minestom.server.extras.selfmodification.mixins;
|
||||||
|
|
||||||
|
import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader;
|
||||||
|
import org.spongepowered.asm.service.IClassProvider;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides classes for Mixin
|
||||||
|
*/
|
||||||
|
public class MinestomClassProvider implements IClassProvider {
|
||||||
|
private final MinestomOverwriteClassLoader classLoader;
|
||||||
|
|
||||||
|
public MinestomClassProvider(MinestomOverwriteClassLoader classLoader) {
|
||||||
|
this.classLoader = classLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL[] getClassPath() {
|
||||||
|
return classLoader.getURLs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> findClass(String name) throws ClassNotFoundException {
|
||||||
|
return classLoader.findClass(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> findClass(String name, boolean initialize) throws ClassNotFoundException {
|
||||||
|
return Class.forName(name, initialize, Thread.currentThread().getContextClassLoader());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> findAgentClass(String name, boolean initialize) throws ClassNotFoundException {
|
||||||
|
return Class.forName(name, initialize, classLoader);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package net.minestom.server.extras.selfmodification.mixins;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.spongepowered.asm.service.IMixinAuditTrail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes care of logging mixin operations
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class MixinAuditTrailMinestom implements IMixinAuditTrail {
|
||||||
|
@Override
|
||||||
|
public void onApply(String className, String mixinName) {
|
||||||
|
log.trace("Applied mixin "+mixinName+" to class "+className);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPostProcess(String className) {
|
||||||
|
log.trace("Post processing "+className);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGenerate(String className, String generatorName) {
|
||||||
|
log.trace("Generating class "+className+" via generator "+generatorName);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package net.minestom.server.extras.selfmodification.mixins;
|
||||||
|
|
||||||
|
import net.minestom.server.extras.selfmodification.CodeModifier;
|
||||||
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
|
import org.spongepowered.asm.mixin.MixinEnvironment;
|
||||||
|
import org.spongepowered.asm.transformers.TreeTransformer;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodeModifier responsible for applying Mixins during class load
|
||||||
|
*/
|
||||||
|
public class MixinCodeModifier extends CodeModifier {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call MixinTransformer's transformClass
|
||||||
|
*/
|
||||||
|
private final Method transformClassMethod;
|
||||||
|
private final TreeTransformer transformer;
|
||||||
|
|
||||||
|
public MixinCodeModifier() {
|
||||||
|
try {
|
||||||
|
// MixinTransformer is package-protected, so we have to force to gain access
|
||||||
|
Class<?> mixinTransformerClass = Class.forName("org.spongepowered.asm.mixin.transformer.MixinTransformer");
|
||||||
|
Constructor<?> ctor = mixinTransformerClass.getDeclaredConstructor();
|
||||||
|
ctor.setAccessible(true);
|
||||||
|
this.transformer = (TreeTransformer) ctor.newInstance();
|
||||||
|
|
||||||
|
// we can't access the MixinTransformer type here, so we use reflection to access the method
|
||||||
|
transformClassMethod = mixinTransformerClass.getDeclaredMethod("transformClass", MixinEnvironment.class, String.class, ClassNode.class);
|
||||||
|
transformClassMethod.setAccessible(true);
|
||||||
|
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException | ClassNotFoundException e) {
|
||||||
|
throw new RuntimeException("Failed to initialize MixinCodeModifier", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean transform(ClassNode source) {
|
||||||
|
try {
|
||||||
|
return (boolean) transformClassMethod.invoke(transformer, MixinEnvironment.getEnvironment(MixinEnvironment.Phase.DEFAULT), source.name.replace("/", "."), source);
|
||||||
|
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getNamespace() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package net.minestom.server.extras.selfmodification.mixins;
|
||||||
|
|
||||||
|
import org.spongepowered.asm.launch.platform.IMixinPlatformServiceAgent;
|
||||||
|
import org.spongepowered.asm.launch.platform.MixinPlatformAgentAbstract;
|
||||||
|
import org.spongepowered.asm.launch.platform.MixinPlatformManager;
|
||||||
|
import org.spongepowered.asm.launch.platform.container.IContainerHandle;
|
||||||
|
import org.spongepowered.asm.util.Constants;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public class MixinPlatformAgentMinestom extends MixinPlatformAgentAbstract implements IMixinPlatformServiceAgent {
|
||||||
|
@Override
|
||||||
|
public void init() { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getSideName() {
|
||||||
|
return Constants.SIDE_SERVER;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AcceptResult accept(MixinPlatformManager manager, IContainerHandle handle) {
|
||||||
|
return AcceptResult.ACCEPTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<IContainerHandle> getMixinContainers() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
package net.minestom.server.extras.selfmodification.mixins;
|
||||||
|
|
||||||
|
import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader;
|
||||||
|
import org.spongepowered.asm.launch.platform.container.ContainerHandleVirtual;
|
||||||
|
import org.spongepowered.asm.launch.platform.container.IContainerHandle;
|
||||||
|
import org.spongepowered.asm.mixin.MixinEnvironment;
|
||||||
|
import org.spongepowered.asm.service.*;
|
||||||
|
import org.spongepowered.asm.util.IConsumer;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public class MixinServiceMinestom extends MixinServiceAbstract {
|
||||||
|
|
||||||
|
private final MinestomOverwriteClassLoader classLoader;
|
||||||
|
private final MinestomClassProvider classProvider;
|
||||||
|
private final MinestomBytecodeProvider bytecodeProvider;
|
||||||
|
private final MixinAuditTrailMinestom auditTrail;
|
||||||
|
private static MixinServiceMinestom INSTANCE = null;
|
||||||
|
private IConsumer<MixinEnvironment.Phase> phaseConsumer;
|
||||||
|
|
||||||
|
public MixinServiceMinestom() {
|
||||||
|
INSTANCE = this;
|
||||||
|
this.classLoader = MinestomOverwriteClassLoader.getInstance();
|
||||||
|
classProvider = new MinestomClassProvider(classLoader);
|
||||||
|
bytecodeProvider = new MinestomBytecodeProvider(classLoader);
|
||||||
|
auditTrail = new MixinAuditTrailMinestom();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Minestom";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IClassProvider getClassProvider() {
|
||||||
|
return classProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IClassBytecodeProvider getBytecodeProvider() {
|
||||||
|
return bytecodeProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ITransformerProvider getTransformerProvider() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<String> getPlatformAgents() {
|
||||||
|
return Collections.singletonList("net.minestom.server.extras.selfmodification.mixins.MixinPlatformAgentMinestom");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IContainerHandle getPrimaryContainer() {
|
||||||
|
return new ContainerHandleVirtual("Minestom");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getResourceAsStream(String name) {
|
||||||
|
return classLoader.getResourceAsStream(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IClassTracker getClassTracker() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IMixinAuditTrail getAuditTrail() {
|
||||||
|
return auditTrail;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void wire(MixinEnvironment.Phase phase, IConsumer<MixinEnvironment.Phase> phaseConsumer) {
|
||||||
|
super.wire(phase, phaseConsumer);
|
||||||
|
this.phaseConsumer = phaseConsumer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void gotoPhase(MixinEnvironment.Phase phase) {
|
||||||
|
phaseConsumer.accept(phase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void gotoPreinitPhase() {
|
||||||
|
if(INSTANCE != null) {
|
||||||
|
INSTANCE.gotoPhase(MixinEnvironment.Phase.PREINIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void gotoInitPhase() {
|
||||||
|
if(INSTANCE != null) {
|
||||||
|
INSTANCE.gotoPhase(MixinEnvironment.Phase.INIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void gotoDefaultPhase() {
|
||||||
|
if(INSTANCE != null) {
|
||||||
|
INSTANCE.gotoPhase(MixinEnvironment.Phase.DEFAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
package net.minestom.server.extras.selfmodification.mixins;
|
||||||
|
|
||||||
|
import org.spongepowered.asm.service.IMixinServiceBootstrap;
|
||||||
|
|
||||||
|
public class MixinServiceMinestomBootstrap implements IMixinServiceBootstrap {
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "MinestomBootstrap";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getServiceClassName() {
|
||||||
|
return "net.minestom.server.extras.selfmodification.mixins.MixinServiceMinestom";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bootstrap() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
net.minestom.server.extras.selfmodification.mixins.GlobalPropertyServiceMinestom
|
@ -0,0 +1 @@
|
|||||||
|
net.minestom.server.extras.selfmodification.mixins.MixinServiceMinestom
|
@ -0,0 +1 @@
|
|||||||
|
net.minestom.server.extras.selfmodification.mixins.MixinServiceMinestomBootstrap
|
@ -2,13 +2,16 @@
|
|||||||
<Configuration status="WARN" strict="true" name="Minestom">
|
<Configuration status="WARN" strict="true" name="Minestom">
|
||||||
<Appenders>
|
<Appenders>
|
||||||
<Appender type="Console" name="STDOUT">
|
<Appender type="Console" name="STDOUT">
|
||||||
<Layout type="PatternLayout" pattern="[%t] [%d{HH:mm:ss}] %p - %m%n"/>
|
<Layout type="PatternLayout" pattern="[%t] [%d{HH:mm:ss}] - %p - %m%n"/>
|
||||||
|
</Appender>
|
||||||
|
<Appender type="Console" name="STDOUT-WithCaller">
|
||||||
|
<Layout type="PatternLayout" pattern="[%t] [%d{HH:mm:ss}] (%C{1}.%M) - %p - %m%n"/>
|
||||||
</Appender>
|
</Appender>
|
||||||
</Appenders>
|
</Appenders>
|
||||||
|
|
||||||
<Loggers>
|
<Loggers>
|
||||||
<Root level="trace">
|
<Root level="trace">
|
||||||
<AppenderRef ref="STDOUT" level="info"/>
|
<AppenderRef ref="STDOUT-WithCaller" level="info"/>
|
||||||
</Root>
|
</Root>
|
||||||
</Loggers>
|
</Loggers>
|
||||||
</Configuration>
|
</Configuration>
|
15
src/test/java/testextension/TestExtension.java
Normal file
15
src/test/java/testextension/TestExtension.java
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package testextension;
|
||||||
|
|
||||||
|
import net.minestom.server.extensions.Extension;
|
||||||
|
|
||||||
|
public class TestExtension extends Extension {
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
System.out.println("Hello from extension!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void terminate() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
17
src/test/java/testextension/TestExtensionLauncherArgs.java
Normal file
17
src/test/java/testextension/TestExtensionLauncherArgs.java
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package testextension;
|
||||||
|
|
||||||
|
import net.minestom.server.Bootstrap;
|
||||||
|
|
||||||
|
// To launch with VM arguments:
|
||||||
|
// -Dminestom.extension.indevfolder.classes=build/classes/java/test/ -Dminestom.extension.indevfolder.resources=build/resources/test/
|
||||||
|
public class TestExtensionLauncherArgs {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
String[] argsWithMixins = new String[args.length+2];
|
||||||
|
System.arraycopy(args, 0, argsWithMixins, 0, args.length);
|
||||||
|
argsWithMixins[argsWithMixins.length-2] = "--mixin";
|
||||||
|
argsWithMixins[argsWithMixins.length-1] = "mixins.testextension.json";
|
||||||
|
Bootstrap.bootstrap("fr.themode.demo.MainDemo", argsWithMixins);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package testextension;
|
||||||
|
|
||||||
|
import net.minestom.server.Bootstrap;
|
||||||
|
import org.spongepowered.asm.launch.MixinBootstrap;
|
||||||
|
import org.spongepowered.asm.mixin.Mixins;
|
||||||
|
|
||||||
|
// To launch with VM arguments:
|
||||||
|
// -Dminestom.extension.indevfolder.classes=build/classes/java/test/ -Dminestom.extension.indevfolder.resources=build/resources/test/
|
||||||
|
public class TestExtensionLauncherNoSetup {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
Bootstrap.bootstrap("fr.themode.demo.MainDemo", args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
37
src/test/java/testextension/TestModifier.java
Normal file
37
src/test/java/testextension/TestModifier.java
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package testextension;
|
||||||
|
|
||||||
|
import net.minestom.server.extras.selfmodification.CodeModifier;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
import org.objectweb.asm.tree.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TestModifier extends CodeModifier implements Opcodes {
|
||||||
|
@Override
|
||||||
|
public boolean transform(ClassNode source) {
|
||||||
|
if(source.name.equals("net/minestom/server/instance/InstanceContainer")) {
|
||||||
|
System.out.println("Modifying code of "+source.name);
|
||||||
|
MethodNode constructor = findConstructor(source.methods);
|
||||||
|
constructor.instructions.insert(constructor.instructions.getFirst(), buildInjectionCode());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private InsnList buildInjectionCode() {
|
||||||
|
InsnList list = new InsnList();
|
||||||
|
list.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
|
||||||
|
list.add(new LdcInsnNode("Hello from modified code!!"));
|
||||||
|
list.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodNode findConstructor(List<MethodNode> methods) {
|
||||||
|
return methods.stream().filter(m -> m.name.equals("<init>")).findFirst().orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getNamespace() {
|
||||||
|
return "net.minestom.server";
|
||||||
|
}
|
||||||
|
}
|
18
src/test/java/testextension/mixins/DynamicChunkMixin.java
Normal file
18
src/test/java/testextension/mixins/DynamicChunkMixin.java
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package testextension.mixins;
|
||||||
|
|
||||||
|
import net.minestom.server.instance.DynamicChunk;
|
||||||
|
import net.minestom.server.instance.block.Block;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||||
|
|
||||||
|
@Mixin(DynamicChunk.class)
|
||||||
|
public class DynamicChunkMixin {
|
||||||
|
|
||||||
|
@ModifyVariable(method = "setBlock", at = @At("HEAD"), index = 4, require = 1, argsOnly = true, remap = false)
|
||||||
|
public short oopsAllTnt(short blockStateId) {
|
||||||
|
if(blockStateId != 0)
|
||||||
|
return Block.TNT.getBlockId();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package testextension.mixins;
|
||||||
|
|
||||||
|
import net.minestom.server.data.Data;
|
||||||
|
import net.minestom.server.instance.InstanceContainer;
|
||||||
|
import net.minestom.server.instance.block.Block;
|
||||||
|
import net.minestom.server.instance.block.CustomBlock;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
|
@Mixin(InstanceContainer.class)
|
||||||
|
public class InstanceContainerMixin {
|
||||||
|
|
||||||
|
@Inject(method = "<init>", at = @At("RETURN"))
|
||||||
|
private void onRunHead(CallbackInfo ci) {
|
||||||
|
System.out.println("Hello from Mixin!!!");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
8
src/test/resources/extension.json
Normal file
8
src/test/resources/extension.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"entrypoint": "testextension.TestExtension",
|
||||||
|
"name": "Test extension",
|
||||||
|
"codeModifiers": [
|
||||||
|
"testextension.TestModifier"
|
||||||
|
],
|
||||||
|
"mixinConfig": "mixins.testextension.json"
|
||||||
|
}
|
11
src/test/resources/mixins.testextension.json
Normal file
11
src/test/resources/mixins.testextension.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"required": true,
|
||||||
|
"minVersion": "0.8",
|
||||||
|
"package": "testextension.mixins",
|
||||||
|
"target": "@env(DEFAULT)",
|
||||||
|
"compatibilityLevel": "JAVA_11",
|
||||||
|
"mixins": [
|
||||||
|
"InstanceContainerMixin",
|
||||||
|
"DynamicChunkMixin"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user