diff --git a/src/main/java/emu/grasscutter/plugin/PluginManager.java b/src/main/java/emu/grasscutter/plugin/PluginManager.java index f6f1cfbf7..8575d125e 100644 --- a/src/main/java/emu/grasscutter/plugin/PluginManager.java +++ b/src/main/java/emu/grasscutter/plugin/PluginManager.java @@ -1,54 +1,60 @@ package emu.grasscutter.plugin; import emu.grasscutter.Grasscutter; -import emu.grasscutter.server.event.Event; -import emu.grasscutter.server.event.EventHandler; -import emu.grasscutter.server.event.HandlerPriority; +import emu.grasscutter.server.event.*; import emu.grasscutter.utils.Utils; +import lombok.*; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.InputStreamReader; +import javax.annotation.Nullable; +import java.io.*; import java.lang.reflect.Method; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; +import java.net.*; import java.util.*; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; +import java.util.jar.*; -import static emu.grasscutter.Configuration.*; +import static emu.grasscutter.Configuration.PLUGIN; /** * Manages the server's plugins and the event system. */ public final class PluginManager { - private final Map plugins = new HashMap<>(); - private final List> listeners = new LinkedList<>(); - + /* All loaded plugins. */ + private final Map plugins = new LinkedHashMap<>(); + /* All currently registered listeners per plugin. */ + private final Map>> listeners = new LinkedHashMap<>(); + public PluginManager() { this.loadPlugins(); // Load all plugins from the plugins directory. } + /* Data about an unloaded plugin. */ + @AllArgsConstructor @Getter + static class PluginData { + private Plugin plugin; + private PluginIdentifier identifier; + private URLClassLoader classLoader; + private String[] dependencies; + } + /** * Loads plugins from the config-specified directory. */ private void loadPlugins() { File pluginsDir = new File(Utils.toFilePath(PLUGIN())); - if(!pluginsDir.exists() && !pluginsDir.mkdirs()) { + if (!pluginsDir.exists() && !pluginsDir.mkdirs()) { Grasscutter.getLogger().error("Failed to create plugins directory: " + pluginsDir.getAbsolutePath()); return; } - + File[] files = pluginsDir.listFiles(); - if(files == null) { + if (files == null) { // The directory is empty, there aren't any plugins to load. return; } - + List plugins = Arrays.stream(files) - .filter(file -> file.getName().endsWith(".jar")) - .toList(); + .filter(file -> file.getName().endsWith(".jar")) + .toList(); URL[] pluginNames = new URL[plugins.size()]; plugins.forEach(plugin -> { @@ -59,36 +65,59 @@ public final class PluginManager { } }); + // Create a class loader for the plugins. URLClassLoader classLoader = new URLClassLoader(pluginNames); + // Create a list of plugins that require dependencies. + List dependencies = new ArrayList<>(); - plugins.forEach(plugin -> { + // Initialize all plugins. + for(var plugin : plugins) { try { URL url = plugin.toURI().toURL(); try (URLClassLoader loader = new URLClassLoader(new URL[]{url})) { - URL configFile = loader.findResource("plugin.json"); // Find the plugin.json file for each plugin. + // Find the plugin.json file for each plugin. + URL configFile = loader.findResource("plugin.json"); + // Open the config file for reading. InputStreamReader fileReader = new InputStreamReader(configFile.openStream()); + // Create a plugin config instance from the config file. PluginConfig pluginConfig = Grasscutter.getGsonFactory().fromJson(fileReader, PluginConfig.class); - if(!pluginConfig.validate()) { + // Check if the plugin config is valid. + if (!pluginConfig.validate()) { Utils.logObject(pluginConfig); Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid config file."); return; } + // Create a JAR file instance from the plugin's URL. JarFile jarFile = new JarFile(plugin); + // Load all class files from the JAR file. Enumeration entries = jarFile.entries(); - while(entries.hasMoreElements()) { + while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); - if(entry.isDirectory() || !entry.getName().endsWith(".class") || entry.getName().contains("module-info")) continue; + if (entry.isDirectory() || !entry.getName().endsWith(".class") || entry.getName().contains("module-info")) + continue; String className = entry.getName().replace(".class", "").replace("/", "."); classLoader.loadClass(className); // Use the same class loader for ALL plugins. } - + + // Create a plugin instance. Class pluginClass = classLoader.loadClass(pluginConfig.mainClass); Plugin pluginInstance = (Plugin) pluginClass.getDeclaredConstructor().newInstance(); + // Close the file reader. + fileReader.close(); + + // Check if the plugin has alternate dependencies. + if(pluginConfig.loadAfter.length > 0) { + // Add the plugin to a "load later" list. + dependencies.add(new PluginData( + pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig), + loader, pluginConfig.loadAfter)); + continue; + } + + // Load the plugin. this.loadPlugin(pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig), loader); - - fileReader.close(); // Close the file reader. } catch (ClassNotFoundException ignored) { Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid main class."); } catch (FileNotFoundException ignored) { @@ -97,29 +126,58 @@ public final class PluginManager { } catch (Exception exception) { Grasscutter.getLogger().error("Failed to load plugin: " + plugin.getName(), exception); } - }); + } + + // Load plugins with dependencies. + int depth = 0; final int maxDepth = 30; + while(!dependencies.isEmpty() || depth < maxDepth) { + try { + // Get the next plugin to load. + var pluginData = dependencies.get(0); + // Check if the plugin's dependencies are loaded. + if(!this.plugins.keySet().containsAll(List.of(pluginData.getDependencies()))) { + depth++; // Increase depth counter. + continue; // Continue to next plugin. + } + + // Load the plugin. + this.loadPlugin(pluginData.getPlugin(), pluginData.getIdentifier(), pluginData.getClassLoader()); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to load a plugin.", exception); + } + } } /** * Load the specified plugin. + * * @param plugin The plugin instance. */ private void loadPlugin(Plugin plugin, PluginIdentifier identifier, URLClassLoader classLoader) { Grasscutter.getLogger().info("Loading plugin: " + identifier.name); - + // Add the plugin's identifier. try { Class pluginClass = Plugin.class; Method method = pluginClass.getDeclaredMethod("initializePlugin", PluginIdentifier.class, URLClassLoader.class); - method.setAccessible(true); method.invoke(plugin, identifier, classLoader); method.setAccessible(false); + method.setAccessible(true); + method.invoke(plugin, identifier, classLoader); + method.setAccessible(false); } catch (Exception ignored) { Grasscutter.getLogger().warn("Failed to add plugin identifier: " + identifier.name); } - + // Add the plugin to the list of loaded plugins. this.plugins.put(identifier.name, plugin); + // Create a collection for the plugin's listeners. + this.listeners.put(plugin, new LinkedList<>()); + // Call the plugin's onLoad method. - plugin.onLoad(); + try { + plugin.onLoad(); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to load plugin: " + identifier.name, exception); + } } /** @@ -128,62 +186,121 @@ public final class PluginManager { public void enablePlugins() { this.plugins.forEach((name, plugin) -> { Grasscutter.getLogger().info("Enabling plugin: " + name); - plugin.onEnable(); + try { + plugin.onEnable(); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to enable plugin: " + name, exception); + } }); } - + /** * Disables all registered plugins. */ public void disablePlugins() { this.plugins.forEach((name, plugin) -> { Grasscutter.getLogger().info("Disabling plugin: " + name); - plugin.onDisable(); + try { + plugin.onDisable(); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to disable plugin: " + name, exception); + } }); } /** * Registers a plugin's event listener. + * + * @param plugin The plugin registering the listener. * @param listener The event listener. */ - public void registerListener(EventHandler listener) { - this.listeners.add(listener); + public void registerListener(Plugin plugin, EventHandler listener) { + this.listeners.get(plugin).add(listener); } - + /** * Invoke the provided event on all registered event listeners. + * * @param event The event to invoke. */ public void invokeEvent(Event event) { EnumSet.allOf(HandlerPriority.class) - .forEach(priority -> this.checkAndFilter(event, priority)); + .forEach(priority -> this.checkAndFilter(event, priority)); } /** * Check an event to handlers for the priority. - * @param event The event being called. + * + * @param event The event being called. * @param priority The priority to call for. */ private void checkAndFilter(Event event, HandlerPriority priority) { - this.listeners.stream() - .filter(handler -> handler.handles().isInstance(event)) - .filter(handler -> handler.getPriority() == priority) - .toList().forEach(handler -> this.invokeHandler(event, handler)); + // Create a collection of listeners. + List> listeners = new LinkedList<>(); + + // Add all listeners from every plugin. + this.listeners.values().forEach(listeners::addAll); + + listeners.stream() + // Filter the listeners by priority. + .filter(handler -> handler.handles().isInstance(event)) + .filter(handler -> handler.getPriority() == priority) + // Invoke the event. + .toList().forEach(handler -> this.invokeHandler(event, handler)); } + /** + * Gets a plugin's instance by its name. + * + * @param name The name of the plugin. + * @return Either null, or the plugin's instance. + */ + @Nullable public Plugin getPlugin(String name) { return this.plugins.get(name); } + /** + * Enables a plugin. + * + * @param plugin The plugin to enable. + */ + public void enablePlugin(Plugin plugin) { + try { + // Call the plugin's onEnable method. + plugin.onEnable(); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to enable plugin: " + plugin.getName(), exception); + } + } + + /** + * Disables a plugin. + * + * @param plugin The plugin to disable. + */ + public void disablePlugin(Plugin plugin) { + try { + // Call the plugin's onDisable method. + plugin.onDisable(); + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to disable plugin: " + plugin.getName(), exception); + } + + // Un-register all listeners. + this.listeners.remove(plugin); + } + /** * Performs logic checks then invokes the provided event handler. - * @param event The event passed through to the handler. + * + * @param event The event passed through to the handler. * @param handler The handler to invoke. */ @SuppressWarnings("unchecked") private void invokeHandler(Event event, EventHandler handler) { - if(!event.isCanceled() || - (event.isCanceled() && handler.ignoresCanceled()) + if (!event.isCanceled() || + (event.isCanceled() && handler.ignoresCanceled()) ) handler.getCallback().consume((T) event); } }