diff --git a/.gitignore b/.gitignore index efa5fcc2e..3e3e2abbb 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ gradle-app.setting # gradle/wrapper/gradle-wrapper.properties /src/main/java/com/mcecraft/ + +# When running the demo we generate the extensions folder +extensions/ diff --git a/src/main/java/net/minestom/server/MinecraftServer.java b/src/main/java/net/minestom/server/MinecraftServer.java index eed4447e6..3673984be 100644 --- a/src/main/java/net/minestom/server/MinecraftServer.java +++ b/src/main/java/net/minestom/server/MinecraftServer.java @@ -13,6 +13,8 @@ import net.minestom.server.data.DataType; import net.minestom.server.data.SerializableData; import net.minestom.server.entity.EntityManager; import net.minestom.server.entity.EntityType; +import net.minestom.server.extensions.Extension; +import net.minestom.server.extensions.ExtensionManager; import net.minestom.server.extras.mojangAuth.MojangCrypt; import net.minestom.server.fluids.Fluid; import net.minestom.server.gamedata.loottables.LootTableManager; @@ -33,7 +35,6 @@ import net.minestom.server.network.packet.server.play.PluginMessagePacket; import net.minestom.server.network.packet.server.play.ServerDifficultyPacket; import net.minestom.server.particle.Particle; import net.minestom.server.ping.ResponseDataConsumer; -import net.minestom.server.plugins.PluginManager; import net.minestom.server.potion.PotionType; import net.minestom.server.recipe.RecipeManager; import net.minestom.server.registry.ResourceGatherer; @@ -123,7 +124,7 @@ public class MinecraftServer { private static BiomeManager biomeManager; private static AdvancementManager advancementManager; - private static PluginManager pluginManager; + private static ExtensionManager extensionManager; private static UpdateManager updateManager; private static MinecraftServer minecraftServer; @@ -180,7 +181,7 @@ public class MinecraftServer { updateManager = new UpdateManager(); - pluginManager = PluginManager.getInstance(); + extensionManager = new ExtensionManager(); lootTableManager = new LootTableManager(); tagManager = new TagManager(); @@ -452,8 +453,14 @@ public class MinecraftServer { updateManager.start(); nettyServer.start(address, port); long t1 = -System.nanoTime(); - pluginManager.loadPlugins(); - LOGGER.info("Plugins loaded in " + (t1 + System.nanoTime()) / 1_000_000D + "ms"); + extensionManager.loadExtensionJARs(); + // Init extensions + // TODO: Extensions should handle depending on each other and have a load-order. + extensionManager.getExtensions().forEach(Extension::preInitialize); + extensionManager.getExtensions().forEach(Extension::initialize); + extensionManager.getExtensions().forEach(Extension::postInitialize); + + LOGGER.info("Extensions loaded in " + (t1 + System.nanoTime()) / 1_000_000D + "ms"); LOGGER.info("Minestom server started successfully."); } diff --git a/src/main/java/net/minestom/server/extensions/Extension.java b/src/main/java/net/minestom/server/extensions/Extension.java new file mode 100644 index 000000000..643f848aa --- /dev/null +++ b/src/main/java/net/minestom/server/extensions/Extension.java @@ -0,0 +1,41 @@ +package net.minestom.server.extensions; + +import com.google.gson.JsonObject; +import org.slf4j.Logger; + +public abstract class Extension { + private JsonObject description; + private Logger logger; + + protected Extension() { + + } + + public void preInitialize() { + + } + + public abstract void initialize(); + + public void postInitialize() { + + } + + public void preTerminate() { + + } + + public abstract void terminate(); + + public void postTerminate() { + + } + + public JsonObject getDescription() { + return description; + } + + protected Logger getLogger() { + return logger; + } +} diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java new file mode 100644 index 000000000..efc0b2c38 --- /dev/null +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -0,0 +1,169 @@ +package net.minestom.server.extensions; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.LoggerFactory; + +import java.io.File; +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.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +public class ExtensionManager { + private final Map extensionLoaders = new HashMap<>(); + private final Map extensions = new HashMap<>(); + private final File extensionFolder = new File("extensions"); + + public ExtensionManager() { + } + + public void loadExtensionJARs() { + if (!extensionFolder.exists()) { + if (!extensionFolder.mkdirs()) { + log.error("Could not find or create the extension folder, extensions will not be loaded!"); + return; + } + } + + for (File file : extensionFolder.listFiles()) { + if (file.isDirectory()) { + continue; + } + if (!file.getName().endsWith(".jar")) { + continue; + } + URLClassLoader loader; + try { + URL url = file.toURI().toURL(); + loader = loadJar(url); + extensionLoaders.put(url, loader); + } catch (MalformedURLException e) { + log.error(String.format("Failed to get URL for file %s.", file.getPath())); + return; + } + InputStream extensionInputStream = loader.getResourceAsStream("extension.json"); + if (extensionInputStream == null) { + log.error(String.format("Failed to find extension.json in the file '%s'.", file.getPath())); + return; + } + JsonObject extensionDescription = JsonParser.parseReader(new InputStreamReader(extensionInputStream)).getAsJsonObject(); + + String mainClass = extensionDescription.get("entrypoint").getAsString(); + String extensionName = extensionDescription.get("name").getAsString(); + + if (extensions.containsKey(extensionName.toLowerCase())) { + log.error(String.format("An extension called '%s' has already been registered.", extensionName)); + return; + } + + Class jarClass; + try { + jarClass = Class.forName(mainClass, true, loader); + } catch (ClassNotFoundException e) { + log.error(String.format("Could not find main class '%s' in extension '%s'.", mainClass, extensionName), e); + return; + } + + Class extensionClass; + try { + extensionClass = jarClass.asSubclass(Extension.class); + } catch (ClassCastException e) { + log.error(String.format("Main class '%s' in '%s' does not extend the 'extension superclass'.", mainClass, extensionName), e); + return; + } + + Constructor constructor; + try { + constructor = extensionClass.getDeclaredConstructor(); + // Let's just make it accessible, plugin creators don't have to make this public. + constructor.setAccessible(true); + } catch (NoSuchMethodException e) { + log.error(String.format("Main class '%s' in '%s' does not define a no-args constructor.", mainClass, extensionName), e); + return; + } + Extension extension = null; + try { + // Is annotated with NotNull + extension = constructor.newInstance(); + } catch (InstantiationException e) { + log.error(String.format("Main class '%s' in '%s' cannot be an abstract class.", mainClass, extensionName), e); + return; + } catch (IllegalAccessException ignored) { + // We made it accessible, should not occur + } catch (InvocationTargetException e) { + log.error( + String.format( + "While instantiating the main class '%s' in '%s' an exception was thrown.", mainClass, extensionName + ), e.getTargetException() + ); + return; + } + // Set description + try { + Field descriptionField = extensionClass.getSuperclass().getDeclaredField("description"); + descriptionField.setAccessible(true); + descriptionField.set(extension, extensionDescription); + } catch (IllegalAccessException e) { + // We made it accessible, should not occur + } catch (NoSuchFieldException e) { + log.error(String.format("Main class '%s' in '%s' has no description field.", mainClass, extensionName), e); + return; + } + // Set Logger + try { + Field descriptionField = extensionClass.getSuperclass().getDeclaredField("logger"); + descriptionField.setAccessible(true); + descriptionField.set(extension, LoggerFactory.getLogger(extensionClass)); + } catch (IllegalAccessException e) { + // We made it accessible, should not occur + } catch (NoSuchFieldException e) { + log.error(String.format("Main class '%s' in '%s' has no logger field.", mainClass, extensionName), e); + } + + extensions.put(extensionName.toLowerCase(), extension); + } + } + + /** + * Loads a URL into the classpath. + * + * @param url {@link URL} (usually a JAR) that should be loaded. + */ + @NotNull + public URLClassLoader loadJar(@NotNull URL url) { + return URLClassLoader.newInstance(new URL[]{url}, ExtensionManager.class.getClassLoader()); + } + + @NotNull + public File getExtensionFolder() { + return extensionFolder; + } + + @NotNull + public List getExtensions() { + return new ArrayList<>(extensions.values()); + } + + @Nullable + public Extension getExtension(@NotNull String name) { + return extensions.get(name.toLowerCase()); + } + + @NotNull + public Map getExtensionLoaders() { + return new HashMap<>(extensionLoaders); + } +} diff --git a/src/main/java/net/minestom/server/plugins/Plugin.java b/src/main/java/net/minestom/server/plugins/Plugin.java deleted file mode 100644 index 3bde83176..000000000 --- a/src/main/java/net/minestom/server/plugins/Plugin.java +++ /dev/null @@ -1,5 +0,0 @@ -package net.minestom.server.plugins; - -public abstract class Plugin { - -} diff --git a/src/main/java/net/minestom/server/plugins/PluginDescription.java b/src/main/java/net/minestom/server/plugins/PluginDescription.java deleted file mode 100644 index 63cbbaf74..000000000 --- a/src/main/java/net/minestom/server/plugins/PluginDescription.java +++ /dev/null @@ -1,8 +0,0 @@ -package net.minestom.server.plugins; - -public @interface PluginDescription { - - String name(); - String description(); - String version(); -} diff --git a/src/main/java/net/minestom/server/plugins/PluginLoader.java b/src/main/java/net/minestom/server/plugins/PluginLoader.java deleted file mode 100644 index 6d6af6a0a..000000000 --- a/src/main/java/net/minestom/server/plugins/PluginLoader.java +++ /dev/null @@ -1,70 +0,0 @@ -package net.minestom.server.plugins; - -import lombok.extern.slf4j.Slf4j; - -import java.io.IOException; -import java.lang.reflect.Type; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -@Slf4j -public class PluginLoader { - - private static PluginLoader instance = null; - - //Singleton - public static PluginLoader getInstance() { - if (instance == null) { - instance = new PluginLoader(); - } - return instance; - } - - private PluginLoader() { - - } - - public List loadPlugin(String path) { - JarFile jarFile; - URLClassLoader cl; - try { - jarFile = new JarFile(path); - URL[] urls = new URL[]{new URL("jar:file:" + path + "!/")}; - cl = URLClassLoader.newInstance(urls); - } catch (IOException e) { - e.printStackTrace(); - return new ArrayList<>(); - } - final List plugins = new ArrayList<>(); - final Enumeration e = jarFile.entries(); - while (e.hasMoreElements()) { - try { - final JarEntry je = e.nextElement(); - if (je.isDirectory() || !je.getName().endsWith(".class")) continue; - // -6 because of .class - String className = je.getName().substring(0, je.getName().length() - 6); - className = className.replace('/', '.'); - final Class c; - c = cl.loadClass(className); - Type superclass = c.getGenericSuperclass(); - if (superclass != null && Plugin.class.getTypeName().equals(superclass.getTypeName())) - try { - plugins.add((Plugin) c.getConstructor().newInstance()); - } catch (final ReflectiveOperationException | ArrayIndexOutOfBoundsException ex) { - ex.printStackTrace(); - } - } catch (final Throwable ex) { - ex.printStackTrace(); - } - } - - return Collections.unmodifiableList(plugins); - } - -} diff --git a/src/main/java/net/minestom/server/plugins/PluginManager.java b/src/main/java/net/minestom/server/plugins/PluginManager.java deleted file mode 100644 index 37aaa0d2e..000000000 --- a/src/main/java/net/minestom/server/plugins/PluginManager.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.minestom.server.plugins; - -import lombok.extern.slf4j.Slf4j; - -import java.io.File; - -@Slf4j -public class PluginManager { - - private static PluginManager instance = null; - - //Singleton - public static PluginManager getInstance() { - if (instance == null) { - instance = new PluginManager(); - } - return instance; - } - - private final PluginLoader loader = PluginLoader.getInstance(); - - private final File pluginsDir; - - private PluginManager() { - pluginsDir = new File("plugins"); - if (!pluginsDir.exists()||!pluginsDir.isDirectory()) { - if (!pluginsDir.mkdir()) { - log.error("Couldn't create plugins dir, plugins will not be loaded."); - return; - } - } - } - - public void loadPlugins() { - - File[] files = pluginsDir.listFiles(); - if(files != null) { - for (final File plugin : files) { - loader.loadPlugin(plugin.getPath()); - } - } - } -}