diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index 4a6f71680..7da51f912 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -1,7 +1,5 @@ package emu.grasscutter; -import java.util.ArrayList; - public final class Config { public String DatabaseUrl = "mongodb://localhost:27017"; @@ -12,6 +10,7 @@ public final class Config { public String PACKETS_FOLDER = "./packets/"; public String DUMPS_FOLDER = "./dumps/"; public String KEY_FOLDER = "./keys/"; + public String PLUGINS_FOLDER = "./plugins/"; public String RunMode = "HYBRID"; // HYBRID, DISPATCH_ONLY, GAME_ONLY public GameServerOptions GameServer = new GameServerOptions(); diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index abcdc3557..de7025b12 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -8,6 +8,7 @@ import java.io.InputStreamReader; import java.net.InetSocketAddress; import emu.grasscutter.command.CommandMap; +import emu.grasscutter.plugin.PluginManager; import emu.grasscutter.utils.Utils; import org.reflections.Reflections; import org.slf4j.LoggerFactory; @@ -35,6 +36,7 @@ public final class Grasscutter { private static GameServer gameServer; public static final Reflections reflector = new Reflections(); + public static final PluginManager pluginManager; static { // Declare logback configuration. @@ -45,6 +47,9 @@ public final class Grasscutter { // Check server structure. Utils.startupCheck(); + + // Call plugin manager. + pluginManager = new PluginManager(); } public static void main(String[] args) throws Exception { @@ -91,8 +96,6 @@ public final class Grasscutter { getLogger().error("Shutting down..."); System.exit(1); } - - // Open console. startConsole(); @@ -161,4 +164,8 @@ public final class Grasscutter { public static GameServer getGameServer() { return gameServer; } + + public static PluginManager getPluginManager() { + return pluginManager; + } } diff --git a/src/main/java/emu/grasscutter/plugin/Plugin.java b/src/main/java/emu/grasscutter/plugin/Plugin.java new file mode 100644 index 000000000..145a06eb8 --- /dev/null +++ b/src/main/java/emu/grasscutter/plugin/Plugin.java @@ -0,0 +1,71 @@ +package emu.grasscutter.plugin; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.server.game.GameServer; + +/** + * The base class for all plugins to extend. + */ +public abstract class Plugin { + private final PluginIdentifier identifier; + + /** + * Empty constructor for developers. + * Should not be called by users. + */ + public Plugin() { + this(new PluginIdentifier("", "", "", new String[]{})); + } + + /** + * Constructor for plugins. + * @param identifier The plugin's identifier. + */ + public Plugin(PluginIdentifier identifier) { + this.identifier = identifier; + } + + /** + * The plugin's identifier instance. + * @return An instance of {@link PluginIdentifier}. + */ + public final PluginIdentifier getIdentifier(){ + return this.identifier; + } + + /** + * Get the plugin's name. + */ + public final String getName() { + return this.identifier.name; + } + + /** + * Get the plugin's description. + */ + public final String getDescription() { + return this.identifier.description; + } + + /** + * Get the plugin's version. + */ + public final String getVersion() { + return this.identifier.version; + } + + /** + * Returns the server that initialized the plugin. + * @return A server instance. + */ + public final GameServer getServer() { + return Grasscutter.getGameServer(); + } + + /* Called when the plugin is first loaded. */ + public void onLoad() { } + /* Called after (most of) the server enables. */ + public void onEnable() { } + /* Called before the server disables. */ + public void onDisable() { } +} diff --git a/src/main/java/emu/grasscutter/plugin/PluginConfig.java b/src/main/java/emu/grasscutter/plugin/PluginConfig.java new file mode 100644 index 000000000..0fb07037c --- /dev/null +++ b/src/main/java/emu/grasscutter/plugin/PluginConfig.java @@ -0,0 +1,18 @@ +package emu.grasscutter.plugin; + +/** + * The data contained in the plugin's `plugin.json` file. + */ +public final class PluginConfig { + public String name, description, version; + public String mainClass; + public String[] authors; + + /** + * Attempts to validate this config instance. + * @return True if the config is valid, false otherwise. + */ + public boolean validate() { + return name != null && description != null && mainClass != null; + } +} diff --git a/src/main/java/emu/grasscutter/plugin/PluginIdentifier.java b/src/main/java/emu/grasscutter/plugin/PluginIdentifier.java new file mode 100644 index 000000000..a467e3949 --- /dev/null +++ b/src/main/java/emu/grasscutter/plugin/PluginIdentifier.java @@ -0,0 +1,29 @@ +package emu.grasscutter.plugin; + +// TODO: Potentially replace with Lombok? +public final class PluginIdentifier { + public final String name, description, version; + public final String[] authors; + + public PluginIdentifier( + String name, String description, String version, + String[] authors + ) { + this.name = name; + this.description = description; + this.version = version; + this.authors = authors; + } + + /** + * Converts a {@link PluginConfig} into a {@link PluginIdentifier}. + */ + public static PluginIdentifier fromPluginConfig(PluginConfig config) { + if(!config.validate()) + throw new IllegalArgumentException("A valid plugin config is required to convert into a plugin identifier."); + return new PluginIdentifier( + config.name, config.description, config.version, + config.authors + ); + } +} diff --git a/src/main/java/emu/grasscutter/plugin/PluginManager.java b/src/main/java/emu/grasscutter/plugin/PluginManager.java new file mode 100644 index 000000000..3177bb291 --- /dev/null +++ b/src/main/java/emu/grasscutter/plugin/PluginManager.java @@ -0,0 +1,102 @@ +package emu.grasscutter.plugin; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.utils.Utils; + +import java.io.File; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Manages the server's plugins & the event system. + */ +public final class PluginManager { + private final Map plugins = new HashMap<>(); + + public PluginManager() { + this.loadPlugins(); // Load all plugins from the plugins directory. + } + + /** + * Loads plugins from the config-specified directory. + */ + private void loadPlugins() { + String directory = Grasscutter.getConfig().PLUGINS_FOLDER; + File pluginsDir = new File(Utils.toFilePath(directory)); + if(!pluginsDir.exists() && !pluginsDir.mkdirs()) { + Grasscutter.getLogger().error("Failed to create plugins directory: " + pluginsDir.getAbsolutePath()); + return; + } + + File[] files = pluginsDir.listFiles(); + 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")) + .collect(Collectors.toList()); + + plugins.forEach(plugin -> { + try { + URL url = plugin.toURI().toURL(); + try (URLClassLoader loader = new URLClassLoader(new URL[]{url})) { + URL configFile = loader.findResource("plugin.json"); + InputStreamReader fileReader = new InputStreamReader(configFile.openStream()); + + PluginConfig pluginConfig = Grasscutter.getGsonFactory().fromJson(fileReader, PluginConfig.class); + if(!pluginConfig.validate()) { + Utils.logObject(pluginConfig); + Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid config file."); + return; + } + + Class pluginClass = loader.loadClass(pluginConfig.mainClass); + Plugin pluginInstance = (Plugin) pluginClass.getDeclaredConstructor(PluginIdentifier.class) + .newInstance(PluginIdentifier.fromPluginConfig(pluginConfig)); + this.loadPlugin(pluginInstance); + + fileReader.close(); // Close the file reader. + } catch (ClassNotFoundException ignored) { + Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid main class."); + } + } catch (Exception exception) { + Grasscutter.getLogger().error("Failed to load plugin: " + plugin.getName(), exception); + } + }); + } + + /** + * Load the specified plugin. + * @param plugin The plugin instance. + */ + private void loadPlugin(Plugin plugin) { + Grasscutter.getLogger().info("Loading plugin: " + plugin.getName()); + + // Add the plugin to the list of loaded plugins. + this.plugins.put(plugin.getName(), plugin); + // Call the plugin's onLoad method. + plugin.onLoad(); + } + + /** + * Enables all registered plugins. + */ + public void enablePlugins() { + + } + + /** + * Disables all registered plugins. + */ + public void disablePlugins() { + + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/utils/Utils.java b/src/main/java/emu/grasscutter/utils/Utils.java index c481ffd07..5b7b8c439 100644 --- a/src/main/java/emu/grasscutter/utils/Utils.java +++ b/src/main/java/emu/grasscutter/utils/Utils.java @@ -137,6 +137,15 @@ public final class Utils { return nonNull != null ? nonNull : fallback; } + /** + * Logs an object to the console. + * @param object The object to log. + */ + public static void logObject(Object object) { + String asJson = Grasscutter.getGsonFactory().toJson(object); + Grasscutter.getLogger().info(asJson); + } + /** * Checks for required files and folders before startup. */