diff --git a/.gitignore b/.gitignore index 3e3e2abbb..b1a8308b6 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,4 @@ gradle-app.setting /src/main/java/com/mcecraft/ # When running the demo we generate the extensions folder -extensions/ +/extensions/ diff --git a/build.gradle b/build.gradle index 16df9c413..57ffd461d 100644 --- a/build.gradle +++ b/build.gradle @@ -141,6 +141,8 @@ dependencies { api 'com.github.MadMartian:hydrazine-path-finding:1.4.2' api "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + + // NBT parsing/manipulation/saving api("com.github.jglrxavpok:Hephaistos:${project.hephaistos_version}") api("com.github.jglrxavpok:Hephaistos:${project.hephaistos_version}:gson") api("com.github.jglrxavpok:Hephaistos:${project.hephaistos_version}") { @@ -149,6 +151,8 @@ dependencies { } } + implementation "com.github.Minestom:DependencyGetter:v1.0.1" + // LWJGL, for map rendering lwjglApi platform("org.lwjgl:lwjgl-bom:$lwjglVersion") diff --git a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java new file mode 100644 index 000000000..094345266 --- /dev/null +++ b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java @@ -0,0 +1,120 @@ +package net.minestom.server.extensions; + +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; + +@Slf4j +class DiscoveredExtension { + private static String NAME_REGEX = "[A-Za-z][_A-Za-z0-9]+"; + + enum LoadStatus { + LOAD_SUCCESS("Actually, it did not fail. This message should not have been printed."), + MISSING_DEPENDENCIES("Missing dependencies, check your logs."), + INVALID_NAME("Invalid name."), + NO_ENTRYPOINT("No entrypoint specified."), + ; + + private final String message; + + LoadStatus(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } + + static class Dependencies { + static class Repository { + String name; + String url; + } + + Repository[] repositories; + String[] artifacts; + } + + transient File[] files = new File[0]; + transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS; + + private String[] codeModifiers; + private String[] authors; + private String mixinConfig; + private String name; + private String version; + private String entrypoint; + private Dependencies dependencies; + + void checkIntegrity() { + if(name == null) { + StringBuilder fileList = new StringBuilder(); + for(File f : files) { + fileList.append(f.getAbsolutePath()).append(", "); + } + log.error("Extension with no name. (at {}})", fileList); + log.error("Extension at ({}) will not be loaded.", fileList); + loadStatus = LoadStatus.INVALID_NAME; + return; + } + if(!name.matches(NAME_REGEX)) { + log.error("Extension '{}' specified an invalid name.", name); + log.error("Extension '{}' will not be loaded.", name); + loadStatus = LoadStatus.INVALID_NAME; + return; + } + if(entrypoint == null) { + log.error("Extension '{}' did not specify an entry point (via 'entrypoint').", name); + log.error("Extension '{}' will not be loaded.", name); + loadStatus = LoadStatus.NO_ENTRYPOINT; + return; + } + if(codeModifiers == null) { + codeModifiers = new String[0]; + } + } + + @NotNull + public String getName() { + if(name == null) { + throw new IllegalStateException("Missing extension name"); + } + return name; + } + + @NotNull + public String[] getCodeModifiers() { + if(codeModifiers == null) { + codeModifiers = new String[0]; + } + return codeModifiers; + } + + @Nullable + public String getMixinConfig() { + return mixinConfig; + } + + @Nullable + public String[] getAuthors() { + return authors; + } + + @Nullable + public String getVersion() { + return version; + } + + @NotNull + public String getEntrypoint() { + return entrypoint; + } + + @Nullable + public Dependencies getDependencies() { + return dependencies; + } +} diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index 699622633..364c969e8 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -1,11 +1,9 @@ package net.minestom.server.extensions; import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; +import net.minestom.dependencies.DependencyGetter; +import net.minestom.dependencies.maven.MavenRepository; import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; @@ -16,20 +14,15 @@ import org.spongepowered.asm.mixin.Mixins; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.zip.ZipFile; @Slf4j @@ -42,6 +35,7 @@ public class ExtensionManager { private final Map extensionLoaders = new HashMap<>(); private final Map extensions = new HashMap<>(); private final File extensionFolder = new File("extensions"); + private final File dependenciesFolder = new File(extensionFolder, ".libs"); private boolean loaded; public ExtensionManager() { @@ -58,7 +52,17 @@ public class ExtensionManager { } } + if (!dependenciesFolder.exists()) { + if (!dependenciesFolder.mkdirs()) { + log.error("Could not find nor create the extension dependencies folder, extensions will not be loaded!"); + return; + } + } + final List discoveredExtensions = discoverExtensions(); + loadDependencies(discoveredExtensions); + // remove invalid extensions + discoveredExtensions.removeIf(ext -> ext.loadStatus != DiscoveredExtension.LoadStatus.LOAD_SUCCESS); setupCodeModifiers(discoveredExtensions); for (DiscoveredExtension discoveredExtension : discoveredExtensions) { @@ -73,47 +77,25 @@ public class ExtensionManager { log.error("Failed to get URL.", e); continue; } - // TODO: Can't we use discoveredExtension.description here? Someone should test that. - final InputStream extensionInputStream = loader.getResourceAsStream("extension.json"); - if (extensionInputStream == null) { - 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("Failed to find extension.json in the urls '{}'.", urlsString); - continue; - } - JsonObject extensionDescriptionJson = JsonParser.parseReader(new InputStreamReader(extensionInputStream)).getAsJsonObject(); - - final String mainClass = extensionDescriptionJson.get("entrypoint").getAsString(); - final String extensionName = extensionDescriptionJson.get("name").getAsString(); - // Check the validity of the extension's name. - if (!extensionName.matches("[A-Za-z]+")) { - log.error("Extension '{}' specified an invalid name.", extensionName); - log.error("Extension '{}' will not be loaded.", extensionName); - continue; - } // Get ExtensionDescription (authors, version etc.) + String extensionName = discoveredExtension.getName(); + String mainClass = discoveredExtension.getEntrypoint(); Extension.ExtensionDescription extensionDescription; { String version; - if (!extensionDescriptionJson.has("version")) { + if (discoveredExtension.getVersion() == null) { log.warn("Extension '{}' did not specify a version.", extensionName); log.warn("Extension '{}' will continue to load but should specify a plugin version.", extensionName); version = "Not Specified"; } else { - version = extensionDescriptionJson.get("version").getAsString(); + version = discoveredExtension.getVersion(); } List authors; - if (!extensionDescriptionJson.has("authors")) { + if (discoveredExtension.getAuthors() == null) { authors = new ArrayList<>(); } else { - authors = Arrays.asList(new Gson().fromJson(extensionDescriptionJson.get("authors"), String[].class)); + authors = Arrays.asList(discoveredExtension.getAuthors()); } extensionDescription = new Extension.ExtensionDescription(extensionName, version, authors); @@ -198,6 +180,57 @@ public class ExtensionManager { } } + private void loadDependencies(List extensions) { + for(DiscoveredExtension ext : extensions) { + try { + DependencyGetter getter = new DependencyGetter(); + DiscoveredExtension.Dependencies dependencies = ext.getDependencies(); + if(dependencies.repositories == null) { + throw new IllegalStateException("Missing 'repositories' array."); + } + if(dependencies.artifacts == null) { + throw new IllegalStateException("Missing 'artifacts' array."); + } + List repoList = new LinkedList<>(); + for(var repository : dependencies.repositories) { + if(repository.name == null) { + throw new IllegalStateException("Missing 'name' element in repository object."); + } + if(repository.url == null) { + throw new IllegalStateException("Missing 'url' element in repository object."); + } + repoList.add(new MavenRepository(repository.name, repository.url)); + } + getter.addMavenResolver(repoList); + + for(var artifact : dependencies.artifacts) { + var resolved = getter.get(artifact, dependenciesFolder); + injectIntoClasspath(resolved.getContentsLocation(), ext); + log.trace("Dependency of extension {}: {}", ext.getName(), resolved); + } + } catch (Exception e) { + ext.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES; + log.error("Failed to load dependencies for extension {}", ext.getName()); + log.error("Extension '{}' will not be loaded", ext.getName()); + log.error("This is the exception", e); + } + } + } + + private void injectIntoClasspath(URL dependency, DiscoveredExtension extension) { + final ClassLoader cl = getClass().getClassLoader(); + if (!(cl instanceof URLClassLoader)) { + throw new IllegalStateException("Current class loader is not a URLClassLoader, but " + cl + ". This prevents adding URLs into the classpath at runtime."); + } + try { + Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + addURL.setAccessible(true); + addURL.invoke(cl, dependency); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException("Failed to inject URL "+dependency+" into classpath. From extension "+extension.getName(), e); + } + } + @NotNull private List discoverExtensions() { List extensions = new LinkedList<>(); @@ -211,10 +244,12 @@ public class ExtensionManager { try (ZipFile f = new ZipFile(file); InputStreamReader reader = new InputStreamReader(f.getInputStream(f.getEntry("extension.json")))) { - DiscoveredExtension extension = new DiscoveredExtension(); + DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); extension.files = new File[]{file}; - extension.description = GSON.fromJson(reader, JsonObject.class); - extensions.add(extension); + extension.checkIntegrity(); + if(extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { + extensions.add(extension); + } } catch (IOException e) { e.printStackTrace(); } @@ -226,10 +261,12 @@ public class ExtensionManager { final String extensionClasses = System.getProperty(INDEV_CLASSES_FOLDER); final String extensionResources = System.getProperty(INDEV_RESOURCES_FOLDER); try (InputStreamReader reader = new InputStreamReader(new FileInputStream(new File(extensionResources, "extension.json")))) { - DiscoveredExtension extension = new DiscoveredExtension(); + DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); extension.files = new File[]{new File(extensionClasses), new File(extensionResources)}; - extension.description = GSON.fromJson(reader, JsonObject.class); - extensions.add(extension); + extension.checkIntegrity(); + if(extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { + extensions.add(extension); + } } catch (IOException e) { e.printStackTrace(); } @@ -280,16 +317,13 @@ public class ExtensionManager { log.info("Start loading code modifiers..."); for (DiscoveredExtension extension : extensions) { try { - if (extension.description.has("codeModifiers")) { - final JsonArray codeModifierClasses = extension.description.getAsJsonArray("codeModifiers"); - for (JsonElement elem : codeModifierClasses) { - modifiableClassLoader.loadModifier(extension.files, elem.getAsString()); - } + for (String codeModifierClass : extension.getCodeModifiers()) { + modifiableClassLoader.loadModifier(extension.files, codeModifierClass); } - if (extension.description.has("mixinConfig")) { - final String mixinConfigFile = extension.description.get("mixinConfig").getAsString(); + if (extension.getMixinConfig() != null) { + final String mixinConfigFile = extension.getMixinConfig(); Mixins.addConfiguration(mixinConfigFile); - log.info("Found mixin in extension " + extension.description.get("name").getAsString() + ": " + mixinConfigFile); + log.info("Found mixin in extension " + extension.getName() + ": " + mixinConfigFile); } } catch (Exception e) { e.printStackTrace(); @@ -299,8 +333,5 @@ public class ExtensionManager { log.info("Done loading code modifiers."); } - private static class DiscoveredExtension { - private File[] files; - private JsonObject description; - } + } diff --git a/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java b/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java index 76a07d675..951f20862 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java @@ -44,6 +44,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { add("org.apache"); add("org.spongepowered"); add("net.minestom.server.extras.selfmodification"); + add("org.jboss.shrinkwrap.resolver"); } }; /** @@ -223,6 +224,11 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { } } + @Override + public void addURL(URL url) { + super.addURL(url); + } + public List getModifiers() { return modifiers; } diff --git a/src/test/java/testextension/TestExtensionLauncherArgs.java b/src/test/java/testextension/TestExtensionLauncherArgs.java index f9130f23f..eb36cab23 100644 --- a/src/test/java/testextension/TestExtensionLauncherArgs.java +++ b/src/test/java/testextension/TestExtensionLauncherArgs.java @@ -11,7 +11,7 @@ public class TestExtensionLauncherArgs { 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); + Bootstrap.bootstrap("demo.MainDemo", argsWithMixins); } } diff --git a/src/test/java/testextension/TestExtensionLauncherNoSetup.java b/src/test/java/testextension/TestExtensionLauncherNoSetup.java index a3e358579..ef4b757fe 100644 --- a/src/test/java/testextension/TestExtensionLauncherNoSetup.java +++ b/src/test/java/testextension/TestExtensionLauncherNoSetup.java @@ -9,7 +9,7 @@ import org.spongepowered.asm.mixin.Mixins; public class TestExtensionLauncherNoSetup { public static void main(String[] args) { - Bootstrap.bootstrap("fr.themode.demo.MainDemo", args); + Bootstrap.bootstrap("demo.MainDemo", args); } } diff --git a/src/test/java/testextension/mixins/DynamicChunkMixin.java b/src/test/java/testextension/mixins/DynamicChunkMixin.java index a71075469..468687446 100644 --- a/src/test/java/testextension/mixins/DynamicChunkMixin.java +++ b/src/test/java/testextension/mixins/DynamicChunkMixin.java @@ -9,7 +9,7 @@ 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) + @ModifyVariable(method = "UNSAFE_setBlock", at = @At("HEAD"), index = 4, require = 1, argsOnly = true, remap = false) public short oopsAllTnt(short blockStateId) { if(blockStateId != 0) return Block.TNT.getBlockId(); diff --git a/src/test/resources/extension.json b/src/test/resources/extension.json index c7d404173..46a91d0b9 100644 --- a/src/test/resources/extension.json +++ b/src/test/resources/extension.json @@ -1,6 +1,6 @@ { "entrypoint": "testextension.TestExtension", - "name": "Test extension", + "name": "Test_extension", "codeModifiers": [ "testextension.TestModifier" ],