Commit 1c9a9599 authored by Magix's avatar Magix Committed by GitHub
Browse files

Change plugin manager logic

Merge pull request #1346 from Grasscutters/plugin-priority
parents 74151c20 3585fd59
...@@ -43,7 +43,7 @@ sourceCompatibility = JavaVersion.VERSION_17 ...@@ -43,7 +43,7 @@ sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
group = 'xyz.grasscutters' group = 'xyz.grasscutters'
version = '1.2.1-dev' version = '1.2.2-dev'
sourceCompatibility = 17 sourceCompatibility = 17
......
...@@ -9,7 +9,11 @@ ...@@ -9,7 +9,11 @@
"pattern": "^[A-Za-z\\d_.-]+$" "pattern": "^[A-Za-z\\d_.-]+$"
} }
}, },
"required": [ "name", "description", "mainClass" ], "required": [
"name",
"description",
"mainClass"
],
"properties": { "properties": {
"name": { "name": {
"description": "The unique name of plugin.", "description": "The unique name of plugin.",
...@@ -22,7 +26,10 @@ ...@@ -22,7 +26,10 @@
}, },
"version": { "version": {
"description": "A plugin revision identifier.", "description": "A plugin revision identifier.",
"type": [ "string", "number" ] "type": [
"string",
"number"
]
}, },
"description": { "description": {
"description": "Human readable plugin summary.", "description": "Human readable plugin summary.",
...@@ -44,6 +51,13 @@ ...@@ -44,6 +51,13 @@
"description": "The URL to the plugin's site", "description": "The URL to the plugin's site",
"type": "string", "type": "string",
"format": "uri" "format": "uri"
},
"loadAfter": {
"description": "Plugins to load before this plugin.",
"type": "array",
"items": {
"type": "string"
}
} }
} }
} }
\ No newline at end of file
package emu.grasscutter; package emu.grasscutter;
import java.io.*; import ch.qos.logback.classic.Logger;
import java.util.Calendar; import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import emu.grasscutter.auth.AuthenticationSystem; import emu.grasscutter.auth.AuthenticationSystem;
import emu.grasscutter.auth.DefaultAuthentication; import emu.grasscutter.auth.DefaultAuthentication;
import emu.grasscutter.command.CommandMap; import emu.grasscutter.command.CommandMap;
import emu.grasscutter.command.DefaultPermissionHandler; import emu.grasscutter.command.DefaultPermissionHandler;
import emu.grasscutter.command.PermissionHandler; import emu.grasscutter.command.PermissionHandler;
import emu.grasscutter.game.dungeons.challenge.DungeonChallenge; import emu.grasscutter.game.dungeons.challenge.DungeonChallenge;
import emu.grasscutter.data.ResourceLoader;
import emu.grasscutter.database.DatabaseManager;
import emu.grasscutter.game.managers.energy.EnergyManager; import emu.grasscutter.game.managers.energy.EnergyManager;
import emu.grasscutter.game.managers.stamina.StaminaManager; import emu.grasscutter.game.managers.stamina.StaminaManager;
import emu.grasscutter.plugin.PluginManager; import emu.grasscutter.plugin.PluginManager;
import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.plugin.api.ServerHook;
import emu.grasscutter.scripts.ScriptLoader; import emu.grasscutter.scripts.ScriptLoader;
import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.http.HttpServer; import emu.grasscutter.server.http.HttpServer;
import emu.grasscutter.server.http.dispatch.DispatchHandler; import emu.grasscutter.server.http.dispatch.DispatchHandler;
import emu.grasscutter.server.http.handlers.*;
import emu.grasscutter.server.http.dispatch.RegionHandler; import emu.grasscutter.server.http.dispatch.RegionHandler;
import emu.grasscutter.server.http.documentation.DocumentationServerHandler; import emu.grasscutter.server.http.documentation.DocumentationServerHandler;
import emu.grasscutter.server.http.handlers.AnnouncementsHandler;
import emu.grasscutter.server.http.handlers.GachaHandler;
import emu.grasscutter.server.http.handlers.GenericHandler;
import emu.grasscutter.server.http.handlers.LogHandler;
import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.ConfigContainer; import emu.grasscutter.utils.ConfigContainer;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.Language;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import org.jline.reader.EndOfFileException; import org.jline.reader.EndOfFileException;
import org.jline.reader.LineReader; import org.jline.reader.LineReader;
...@@ -30,21 +39,13 @@ import org.jline.terminal.TerminalBuilder; ...@@ -30,21 +39,13 @@ import org.jline.terminal.TerminalBuilder;
import org.reflections.Reflections; import org.reflections.Reflections;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import ch.qos.logback.classic.Logger;
import emu.grasscutter.data.ResourceLoader;
import emu.grasscutter.database.DatabaseManager;
import emu.grasscutter.utils.Language;
import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.Crypto;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.*;
import java.util.Calendar;
import static emu.grasscutter.Configuration.DATA;
import static emu.grasscutter.Configuration.SERVER;
import static emu.grasscutter.utils.Language.translate; import static emu.grasscutter.utils.Language.translate;
import static emu.grasscutter.Configuration.*;
public final class Grasscutter { public final class Grasscutter {
private static final Logger log = (Logger) LoggerFactory.getLogger(Grasscutter.class); private static final Logger log = (Logger) LoggerFactory.getLogger(Grasscutter.class);
...@@ -90,19 +91,22 @@ public final class Grasscutter { ...@@ -90,19 +91,22 @@ public final class Grasscutter {
for (String arg : args) { for (String arg : args) {
switch (arg.toLowerCase()) { switch (arg.toLowerCase()) {
case "-handbook" -> { case "-handbook" -> {
Tools.createGmHandbook(); exitEarly = true; Tools.createGmHandbook();
exitEarly = true;
} }
case "-gachamap" -> { case "-gachamap" -> {
Tools.createGachaMapping(DATA("gacha_mappings.js")); exitEarly = true; Tools.createGachaMapping(DATA("gacha_mappings.js"));
exitEarly = true;
} }
case "-version" -> { case "-version" -> {
System.out.println("Grasscutter version: " + BuildConfig.VERSION + "-" + BuildConfig.GIT_HASH); exitEarly = true; System.out.println("Grasscutter version: " + BuildConfig.VERSION + "-" + BuildConfig.GIT_HASH);
exitEarly = true;
} }
} }
} }
// Exit early if argument sets it. // Exit early if argument sets it.
if(exitEarly) System.exit(0); if (exitEarly) System.exit(0);
// Initialize server. // Initialize server.
Grasscutter.getLogger().info(translate("messages.status.starting")); Grasscutter.getLogger().info(translate("messages.status.starting"));
...@@ -113,8 +117,6 @@ public final class Grasscutter { ...@@ -113,8 +117,6 @@ public final class Grasscutter {
Grasscutter.updateDayOfWeek(); Grasscutter.updateDayOfWeek();
ResourceLoader.loadAll(); ResourceLoader.loadAll();
ScriptLoader.init(); ScriptLoader.init();
EnergyManager.initialize();
DungeonChallenge.initialize();
// Initialize database. // Initialize database.
DatabaseManager.initialize(); DatabaseManager.initialize();
...@@ -142,9 +144,6 @@ public final class Grasscutter { ...@@ -142,9 +144,6 @@ public final class Grasscutter {
httpServer.addRouter(GachaHandler.class); httpServer.addRouter(GachaHandler.class);
httpServer.addRouter(DocumentationServerHandler.class); httpServer.addRouter(DocumentationServerHandler.class);
// TODO: find a better place?
StaminaManager.initialize();
// Start servers. // Start servers.
var runMode = SERVER.runMode; var runMode = SERVER.runMode;
if (runMode == ServerRunMode.HYBRID) { if (runMode == ServerRunMode.HYBRID) {
...@@ -176,6 +175,7 @@ public final class Grasscutter { ...@@ -176,6 +175,7 @@ public final class Grasscutter {
*/ */
private static void onShutdown() { private static void onShutdown() {
// Disable all plugins. // Disable all plugins.
if(pluginManager != null)
pluginManager.disablePlugins(); pluginManager.disablePlugins();
} }
...@@ -215,10 +215,11 @@ public final class Grasscutter { ...@@ -215,10 +215,11 @@ public final class Grasscutter {
/** /**
* Saves the provided server configuration. * Saves the provided server configuration.
*
* @param config The configuration to save, or null for a new one. * @param config The configuration to save, or null for a new one.
*/ */
public static void saveConfig(@Nullable ConfigContainer config) { public static void saveConfig(@Nullable ConfigContainer config) {
if(config == null) config = new ConfigContainer(); if (config == null) config = new ConfigContainer();
try (FileWriter file = new FileWriter(configFile)) { try (FileWriter file = new FileWriter(configFile)) {
file.write(gson.toJson(config)); file.write(gson.toJson(config));
...@@ -350,6 +351,7 @@ public final class Grasscutter { ...@@ -350,6 +351,7 @@ public final class Grasscutter {
/** /**
* Sets the authentication system for the server. * Sets the authentication system for the server.
*
* @param authenticationSystem The authentication system to use. * @param authenticationSystem The authentication system to use.
*/ */
public static void setAuthenticationSystem(AuthenticationSystem authenticationSystem) { public static void setAuthenticationSystem(AuthenticationSystem authenticationSystem) {
...@@ -358,6 +360,7 @@ public final class Grasscutter { ...@@ -358,6 +360,7 @@ public final class Grasscutter {
/** /**
* Sets the permission handler for the server. * Sets the permission handler for the server.
*
* @param permissionHandler The permission handler to use. * @param permissionHandler The permission handler to use.
*/ */
public static void setPermissionHandler(PermissionHandler permissionHandler) { public static void setPermissionHandler(PermissionHandler permissionHandler) {
......
package emu.grasscutter.command; package emu.grasscutter.command;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import org.reflections.Reflections; import org.reflections.Reflections;
import java.util.*; import java.util.*;
...@@ -11,6 +9,7 @@ import java.util.*; ...@@ -11,6 +9,7 @@ import java.util.*;
@SuppressWarnings({"UnusedReturnValue", "unused"}) @SuppressWarnings({"UnusedReturnValue", "unused"})
public final class CommandMap { public final class CommandMap {
private final Map<String, CommandHandler> commands = new HashMap<>(); private final Map<String, CommandHandler> commands = new HashMap<>();
private final Map<String, CommandHandler> aliases = new HashMap<>();
private final Map<String, Command> annotations = new HashMap<>(); private final Map<String, Command> annotations = new HashMap<>();
private final Map<String, Integer> targetPlayerIds = new HashMap<>(); private final Map<String, Integer> targetPlayerIds = new HashMap<>();
private static final String consoleId = "console"; private static final String consoleId = "console";
...@@ -45,7 +44,7 @@ public final class CommandMap { ...@@ -45,7 +44,7 @@ public final class CommandMap {
// Register aliases. // Register aliases.
if (annotation.aliases().length > 0) { if (annotation.aliases().length > 0) {
for (String alias : annotation.aliases()) { for (String alias : annotation.aliases()) {
this.commands.put(alias, command); this.aliases.put(alias, command);
this.annotations.put(alias, annotation); this.annotations.put(alias, annotation);
} }
} }
...@@ -60,6 +59,7 @@ public final class CommandMap { ...@@ -60,6 +59,7 @@ public final class CommandMap {
*/ */
public CommandMap unregisterCommand(String label) { public CommandMap unregisterCommand(String label) {
Grasscutter.getLogger().debug("Unregistered command: " + label); Grasscutter.getLogger().debug("Unregistered command: " + label);
CommandHandler handler = this.commands.get(label); CommandHandler handler = this.commands.get(label);
if (handler == null) return this; if (handler == null) return this;
...@@ -70,7 +70,7 @@ public final class CommandMap { ...@@ -70,7 +70,7 @@ public final class CommandMap {
// Unregister aliases. // Unregister aliases.
if (annotation.aliases().length > 0) { if (annotation.aliases().length > 0) {
for (String alias : annotation.aliases()) { for (String alias : annotation.aliases()) {
this.commands.remove(alias); this.aliases.remove(alias);
this.annotations.remove(alias); this.annotations.remove(alias);
} }
} }
...@@ -78,7 +78,9 @@ public final class CommandMap { ...@@ -78,7 +78,9 @@ public final class CommandMap {
return this; return this;
} }
public List<Command> getAnnotationsAsList() { return new LinkedList<>(this.annotations.values()); } public List<Command> getAnnotationsAsList() {
return new LinkedList<>(this.annotations.values());
}
public HashMap<String, Command> getAnnotations() { public HashMap<String, Command> getAnnotations() {
return new LinkedHashMap<>(this.annotations); return new LinkedHashMap<>(this.annotations);
...@@ -142,7 +144,7 @@ public final class CommandMap { ...@@ -142,7 +144,7 @@ public final class CommandMap {
} }
if (targetUidStr != null) { if (targetUidStr != null) {
if (targetUidStr.equals("")) { // Clears the default targetPlayer. if (targetUidStr.equals("")) { // Clears the default targetPlayer.
targetPlayerIds.remove(playerId); this.targetPlayerIds.remove(playerId);
CommandHandler.sendTranslatedMessage(player, "commands.execution.clear_target"); CommandHandler.sendTranslatedMessage(player, "commands.execution.clear_target");
} else { // Sets default targetPlayer to the UID provided. } else { // Sets default targetPlayer to the UID provided.
try { try {
...@@ -151,9 +153,9 @@ public final class CommandMap { ...@@ -151,9 +153,9 @@ public final class CommandMap {
if (targetPlayer == null) { if (targetPlayer == null) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error"); CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
} else { } else {
targetPlayerIds.put(playerId, uid); this.targetPlayerIds.put(playerId, uid);
CommandHandler.sendTranslatedMessage(player, "commands.execution.set_target", targetUidStr); CommandHandler.sendTranslatedMessage(player, "commands.execution.set_target", targetUidStr);
CommandHandler.sendTranslatedMessage(player, targetPlayer.isOnline()? "commands.execution.set_target_online" : "commands.execution.set_target_offline", targetUidStr); CommandHandler.sendTranslatedMessage(player, targetPlayer.isOnline() ? "commands.execution.set_target_online" : "commands.execution.set_target_offline", targetUidStr);
} }
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.uid_error"); CommandHandler.sendTranslatedMessage(player, "commands.execution.uid_error");
...@@ -164,11 +166,19 @@ public final class CommandMap { ...@@ -164,11 +166,19 @@ public final class CommandMap {
// Get command handler. // Get command handler.
CommandHandler handler = this.commands.get(label); CommandHandler handler = this.commands.get(label);
if(handler == null)
// Try to get the handler by alias.
handler = this.aliases.get(label);
// Check if the handler is still null.
if (handler == null) { if (handler == null) {
CommandHandler.sendTranslatedMessage(player, "commands.generic.unknown_command", label); CommandHandler.sendTranslatedMessage(player, "commands.generic.unknown_command", label);
return; return;
} }
// Get the command's annotation.
Command annotation = this.annotations.get(label);
// If any @UID argument is present, override targetPlayer with it. // If any @UID argument is present, override targetPlayer with it.
for (int i = 0; i < args.size(); i++) { for (int i = 0; i < args.size(); i++) {
String arg = args.get(i); String arg = args.get(i);
...@@ -191,8 +201,8 @@ public final class CommandMap { ...@@ -191,8 +201,8 @@ public final class CommandMap {
// If there's still no targetPlayer at this point, use previously-set target // If there's still no targetPlayer at this point, use previously-set target
if (targetPlayer == null) { if (targetPlayer == null) {
if (targetPlayerIds.containsKey(playerId)) { if (this.targetPlayerIds.containsKey(playerId)) {
targetPlayer = Grasscutter.getGameServer().getPlayerByUid(targetPlayerIds.get(playerId), true); // We check every time in case the target is deleted after being targeted targetPlayer = Grasscutter.getGameServer().getPlayerByUid(this.targetPlayerIds.get(playerId), true); // We check every time in case the target is deleted after being targeted
if (targetPlayer == null) { if (targetPlayer == null) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error"); CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
return; return;
...@@ -204,32 +214,36 @@ public final class CommandMap { ...@@ -204,32 +214,36 @@ public final class CommandMap {
} }
// Check for permissions. // Check for permissions.
if (!Grasscutter.getPermissionHandler().checkPermission(player, targetPlayer, this.annotations.get(label).permission(), this.annotations.get(label).permissionTargeted())) { if (!Grasscutter.getPermissionHandler().checkPermission(player, targetPlayer, annotation.permission(), this.annotations.get(label).permissionTargeted())) {
return; return;
} }
// Check if command has unfulfilled constraints on targetPlayer // Check if command has unfulfilled constraints on targetPlayer
Command.TargetRequirement targetRequirement = this.annotations.get(label).targetRequirement(); Command.TargetRequirement targetRequirement = annotation.targetRequirement();
if (targetRequirement != Command.TargetRequirement.NONE) { if (targetRequirement != Command.TargetRequirement.NONE) {
if (targetPlayer == null) { if (targetPlayer == null) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target"); CommandHandler.sendTranslatedMessage(null, "commands.execution.need_target");
return; return;
} }
if ((targetRequirement == Command.TargetRequirement.ONLINE) && !targetPlayer.isOnline()) { if ((targetRequirement == Command.TargetRequirement.ONLINE) && !targetPlayer.isOnline()) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_online"); CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_online");
return; return;
} }
if ((targetRequirement == Command.TargetRequirement.OFFLINE) && targetPlayer.isOnline()) { if ((targetRequirement == Command.TargetRequirement.OFFLINE) && targetPlayer.isOnline()) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_offline"); CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_offline");
return; return;
} }
} }
// Invoke execute method for handler. // Copy player and handler to final properties.
boolean threading = this.annotations.get(label).threading();
final Player targetPlayerF = targetPlayer; // Is there a better way to do this? final Player targetPlayerF = targetPlayer; // Is there a better way to do this?
Runnable runnable = () -> handler.execute(player, targetPlayerF, args); final CommandHandler handlerF = handler; // Is there a better way to do this?
if(threading) {
// Invoke execute method for handler.
Runnable runnable = () -> handlerF.execute(player, targetPlayerF, args);
if (annotation.threading()) {
new Thread(runnable).start(); new Thread(runnable).start();
} else { } else {
runnable.run(); runnable.run();
...@@ -242,10 +256,11 @@ public final class CommandMap { ...@@ -242,10 +256,11 @@ public final class CommandMap {
private void scan() { private void scan() {
Reflections reflector = Grasscutter.reflector; Reflections reflector = Grasscutter.reflector;
Set<Class<?>> classes = reflector.getTypesAnnotatedWith(Command.class); Set<Class<?>> classes = reflector.getTypesAnnotatedWith(Command.class);
classes.forEach(annotated -> { classes.forEach(annotated -> {
try { try {
Command cmdData = annotated.getAnnotation(Command.class); Command cmdData = annotated.getAnnotation(Command.class);
Object object = annotated.newInstance(); Object object = annotated.getDeclaredConstructor().newInstance();
if (object instanceof CommandHandler) if (object instanceof CommandHandler)
this.registerCommand(cmdData.label(), (CommandHandler) object); this.registerCommand(cmdData.label(), (CommandHandler) object);
else Grasscutter.getLogger().error("Class " + annotated.getName() + " is not a CommandHandler!"); else Grasscutter.getLogger().error("Class " + annotated.getName() + " is not a CommandHandler!");
......
...@@ -6,11 +6,11 @@ import emu.grasscutter.tools.Tools; ...@@ -6,11 +6,11 @@ import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import java.io.*; import java.io.FileInputStream;
import java.nio.file.FileSystems; import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.regex.Pattern;
import static emu.grasscutter.Configuration.DATA; import static emu.grasscutter.Configuration.DATA;
...@@ -18,10 +18,11 @@ public class DataLoader { ...@@ -18,10 +18,11 @@ public class DataLoader {
/** /**
* Load a data file by its name. If the file isn't found within the /data directory then it will fallback to the default within the jar resources * Load a data file by its name. If the file isn't found within the /data directory then it will fallback to the default within the jar resources
* @see #load(String, boolean) *
* @param resourcePath The path to the data file to be loaded. * @param resourcePath The path to the data file to be loaded.
* @return InputStream of the data file. * @return InputStream of the data file.
* @throws FileNotFoundException * @throws FileNotFoundException
* @see #load(String, boolean)
*/ */
public static InputStream load(String resourcePath) throws FileNotFoundException { public static InputStream load(String resourcePath) throws FileNotFoundException {
return load(resourcePath, true); return load(resourcePath, true);
...@@ -29,17 +30,18 @@ public class DataLoader { ...@@ -29,17 +30,18 @@ public class DataLoader {
/** /**
* Load a data file by its name. * Load a data file by its name.
*
* @param resourcePath The path to the data file to be loaded. * @param resourcePath The path to the data file to be loaded.
* @param useFallback If the file does not exist in the /data directory, should it use the default file in the jar? * @param useFallback If the file does not exist in the /data directory, should it use the default file in the jar?
* @return InputStream of the data file. * @return InputStream of the data file.
* @throws FileNotFoundException * @throws FileNotFoundException
*/ */
public static InputStream load(String resourcePath, boolean useFallback) throws FileNotFoundException { public static InputStream load(String resourcePath, boolean useFallback) throws FileNotFoundException {
if(Utils.fileExists(DATA(resourcePath))) { if (Utils.fileExists(DATA(resourcePath))) {
// Data is in the resource directory // Data is in the resource directory
return new FileInputStream(DATA(resourcePath)); return new FileInputStream(DATA(resourcePath));
} else { } else {
if(useFallback) { if (useFallback) {
return FileUtils.readResourceAsStream("/defaults/data/" + resourcePath); return FileUtils.readResourceAsStream("/defaults/data/" + resourcePath);
} }
} }
...@@ -53,9 +55,7 @@ public class DataLoader { ...@@ -53,9 +55,7 @@ public class DataLoader {
if (filenames == null) { if (filenames == null) {
Grasscutter.getLogger().error("We were unable to locate your default data files."); Grasscutter.getLogger().error("We were unable to locate your default data files.");
} } else for (Path file : filenames) {
for (Path file : filenames) {
String relativePath = String.valueOf(file).split("defaults[\\\\\\/]data[\\\\\\/]")[1]; String relativePath = String.valueOf(file).split("defaults[\\\\\\/]data[\\\\\\/]")[1];
CheckAndCopyData(relativePath); CheckAndCopyData(relativePath);
...@@ -76,12 +76,12 @@ public class DataLoader { ...@@ -76,12 +76,12 @@ public class DataLoader {
String[] path = name.split("/"); String[] path = name.split("/");
String folder = ""; String folder = "";
for(int i = 0; i < (path.length - 1); i++) { for (int i = 0; i < (path.length - 1); i++) {
folder += path[i] + "/"; folder += path[i] + "/";
// Make sure the current folder exists // Make sure the current folder exists
String folderToCreate = Utils.toFilePath(DATA(folder)); String folderToCreate = Utils.toFilePath(DATA(folder));
if(!Utils.fileExists(folderToCreate)) { if (!Utils.fileExists(folderToCreate)) {
Grasscutter.getLogger().info("Creating data folder '" + folder + "'"); Grasscutter.getLogger().info("Creating data folder '" + folder + "'");
Utils.createFolder(folderToCreate); Utils.createFolder(folderToCreate);
} }
......
...@@ -990,7 +990,7 @@ public class Player { ...@@ -990,7 +990,7 @@ public class Player {
} }
public void interactWith(int gadgetEntityId, GadgetInteractReq req) { public void interactWith(int gadgetEntityId, GadgetInteractReq opType) {
GameEntity entity = getScene().getEntityById(gadgetEntityId); GameEntity entity = getScene().getEntityById(gadgetEntityId);
if (entity == null) { if (entity == null) {
return; return;
...@@ -1023,7 +1023,7 @@ public class Player { ...@@ -1023,7 +1023,7 @@ public class Player {
return; return;
} }
boolean shouldDelete = gadget.getContent().onInteract(this, req); boolean shouldDelete = gadget.getContent().onInteract(this, opType);
if (shouldDelete) { if (shouldDelete) {
entity.getScene().removeEntity(entity); entity.getScene().removeEntity(entity);
......
...@@ -7,11 +7,13 @@ public final class PluginConfig { ...@@ -7,11 +7,13 @@ public final class PluginConfig {
public String name, description, version; public String name, description, version;
public String mainClass; public String mainClass;
public String[] authors; public String[] authors;
public String[] loadAfter;
/** /**
* Attempts to validate this config instance. * Attempts to validate this config instance.
* @return True if the config is valid, false otherwise. * @return True if the config is valid, false otherwise.
*/ */
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean validate() { public boolean validate() {
return name != null && description != null && mainClass != null; return name != null && description != null && mainClass != null;
} }
......
package emu.grasscutter.plugin; package emu.grasscutter.plugin;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.event.Event; import emu.grasscutter.server.event.*;
import emu.grasscutter.server.event.EventHandler;
import emu.grasscutter.server.event.HandlerPriority;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import lombok.*;
import java.io.File; import javax.annotation.Nullable;
import java.io.FileNotFoundException; import java.io.*;
import java.io.InputStreamReader;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.MalformedURLException; import java.net.*;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*; import java.util.*;
import java.util.jar.JarEntry; import java.util.jar.*;
import java.util.jar.JarFile;
import static emu.grasscutter.Configuration.*; import static emu.grasscutter.Configuration.PLUGIN;
/** /**
* Manages the server's plugins and the event system. * Manages the server's plugins and the event system.
*/ */
public final class PluginManager { public final class PluginManager {
private final Map<String, Plugin> plugins = new HashMap<>(); /* All loaded plugins. */
private final List<EventHandler<? extends Event>> listeners = new LinkedList<>(); private final Map<String, Plugin> plugins = new LinkedHashMap<>();
/* All currently registered listeners per plugin. */
private final Map<Plugin, List<EventHandler<? extends Event>>> listeners = new LinkedHashMap<>();
public PluginManager() { public PluginManager() {
this.loadPlugins(); // Load all plugins from the plugins directory. 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. * Loads plugins from the config-specified directory.
*/ */
private void loadPlugins() { private void loadPlugins() {
File pluginsDir = new File(Utils.toFilePath(PLUGIN())); 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()); Grasscutter.getLogger().error("Failed to create plugins directory: " + pluginsDir.getAbsolutePath());
return; return;
} }
File[] files = pluginsDir.listFiles(); File[] files = pluginsDir.listFiles();
if(files == null) { if (files == null) {
// The directory is empty, there aren't any plugins to load. // The directory is empty, there aren't any plugins to load.
return; return;
} }
...@@ -59,36 +65,59 @@ public final class PluginManager { ...@@ -59,36 +65,59 @@ public final class PluginManager {
} }
}); });
// Create a class loader for the plugins.
URLClassLoader classLoader = new URLClassLoader(pluginNames); URLClassLoader classLoader = new URLClassLoader(pluginNames);
// Create a list of plugins that require dependencies.
List<PluginData> dependencies = new ArrayList<>();
plugins.forEach(plugin -> { // Initialize all plugins.
for(var plugin : plugins) {
try { try {
URL url = plugin.toURI().toURL(); URL url = plugin.toURI().toURL();
try (URLClassLoader loader = new URLClassLoader(new URL[]{url})) { 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()); InputStreamReader fileReader = new InputStreamReader(configFile.openStream());
// Create a plugin config instance from the config file.
PluginConfig pluginConfig = Grasscutter.getGsonFactory().fromJson(fileReader, PluginConfig.class); PluginConfig pluginConfig = Grasscutter.getGsonFactory().fromJson(fileReader, PluginConfig.class);
if(!pluginConfig.validate()) { // Check if the plugin config is valid.
if (!pluginConfig.validate()) {
Utils.logObject(pluginConfig); Utils.logObject(pluginConfig);
Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid config file."); Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid config file.");
return; return;
} }
// Create a JAR file instance from the plugin's URL.
JarFile jarFile = new JarFile(plugin); JarFile jarFile = new JarFile(plugin);
// Load all class files from the JAR file.
Enumeration<JarEntry> entries = jarFile.entries(); Enumeration<JarEntry> entries = jarFile.entries();
while(entries.hasMoreElements()) { while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement(); 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("/", "."); String className = entry.getName().replace(".class", "").replace("/", ".");
classLoader.loadClass(className); // Use the same class loader for ALL plugins. classLoader.loadClass(className); // Use the same class loader for ALL plugins.
} }
// Create a plugin instance.
Class<?> pluginClass = classLoader.loadClass(pluginConfig.mainClass); Class<?> pluginClass = classLoader.loadClass(pluginConfig.mainClass);
Plugin pluginInstance = (Plugin) pluginClass.getDeclaredConstructor().newInstance(); Plugin pluginInstance = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
this.loadPlugin(pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig), loader); // Close the file reader.
fileReader.close();
// Check if the plugin has alternate dependencies.
if(pluginConfig.loadAfter != null && pluginConfig.loadAfter.length > 0) {
// Add the plugin to a "load later" list.
dependencies.add(new PluginData(
pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig),
loader, pluginConfig.loadAfter));
continue;
}
fileReader.close(); // Close the file reader. // Load the plugin.
this.loadPlugin(pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig), loader);
} catch (ClassNotFoundException ignored) { } catch (ClassNotFoundException ignored) {
Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid main class."); Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid main class.");
} catch (FileNotFoundException ignored) { } catch (FileNotFoundException ignored) {
...@@ -97,11 +126,41 @@ public final class PluginManager { ...@@ -97,11 +126,41 @@ public final class PluginManager {
} catch (Exception exception) { } catch (Exception exception) {
Grasscutter.getLogger().error("Failed to load plugin: " + plugin.getName(), 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()) {
// Check if the depth is too high.
if(depth >= maxDepth) {
Grasscutter.getLogger().error("Failed to load plugins with dependencies.");
break;
}
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.
}
// Remove the plugin from the list of dependencies.
dependencies.remove(pluginData);
// Load the plugin.
this.loadPlugin(pluginData.getPlugin(), pluginData.getIdentifier(), pluginData.getClassLoader());
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to load a plugin.", exception); depth++;
}
}
} }
/** /**
* Load the specified plugin. * Load the specified plugin.
*
* @param plugin The plugin instance. * @param plugin The plugin instance.
*/ */
private void loadPlugin(Plugin plugin, PluginIdentifier identifier, URLClassLoader classLoader) { private void loadPlugin(Plugin plugin, PluginIdentifier identifier, URLClassLoader classLoader) {
...@@ -111,15 +170,24 @@ public final class PluginManager { ...@@ -111,15 +170,24 @@ public final class PluginManager {
try { try {
Class<Plugin> pluginClass = Plugin.class; Class<Plugin> pluginClass = Plugin.class;
Method method = pluginClass.getDeclaredMethod("initializePlugin", PluginIdentifier.class, URLClassLoader.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) { } catch (Exception ignored) {
Grasscutter.getLogger().warn("Failed to add plugin identifier: " + identifier.name); Grasscutter.getLogger().warn("Failed to add plugin identifier: " + identifier.name);
} }
// Add the plugin to the list of loaded plugins. // Add the plugin to the list of loaded plugins.
this.plugins.put(identifier.name, plugin); 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. // Call the plugin's onLoad method.
try {
plugin.onLoad(); plugin.onLoad();
} catch (Throwable exception) {
Grasscutter.getLogger().error("Failed to load plugin: " + identifier.name, exception);
}
} }
/** /**
...@@ -128,7 +196,11 @@ public final class PluginManager { ...@@ -128,7 +196,11 @@ public final class PluginManager {
public void enablePlugins() { public void enablePlugins() {
this.plugins.forEach((name, plugin) -> { this.plugins.forEach((name, plugin) -> {
Grasscutter.getLogger().info("Enabling plugin: " + name); Grasscutter.getLogger().info("Enabling plugin: " + name);
try {
plugin.onEnable(); plugin.onEnable();
} catch (Throwable exception) {
Grasscutter.getLogger().error("Failed to enable plugin: " + name, exception);
}
}); });
} }
...@@ -138,20 +210,27 @@ public final class PluginManager { ...@@ -138,20 +210,27 @@ public final class PluginManager {
public void disablePlugins() { public void disablePlugins() {
this.plugins.forEach((name, plugin) -> { this.plugins.forEach((name, plugin) -> {
Grasscutter.getLogger().info("Disabling plugin: " + name); Grasscutter.getLogger().info("Disabling plugin: " + name);
try {
plugin.onDisable(); plugin.onDisable();
} catch (Throwable exception) {
Grasscutter.getLogger().error("Failed to disable plugin: " + name, exception);
}
}); });
} }
/** /**
* Registers a plugin's event listener. * Registers a plugin's event listener.
*
* @param plugin The plugin registering the listener.
* @param listener The event listener. * @param listener The event listener.
*/ */
public void registerListener(EventHandler<? extends Event> listener) { public void registerListener(Plugin plugin, EventHandler<? extends Event> listener) {
this.listeners.add(listener); this.listeners.get(plugin).add(listener);
} }
/** /**
* Invoke the provided event on all registered event listeners. * Invoke the provided event on all registered event listeners.
*
* @param event The event to invoke. * @param event The event to invoke.
*/ */
public void invokeEvent(Event event) { public void invokeEvent(Event event) {
...@@ -161,33 +240,76 @@ public final class PluginManager { ...@@ -161,33 +240,76 @@ public final class PluginManager {
/** /**
* Check an event to handlers for the 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. * @param priority The priority to call for.
*/ */
private void checkAndFilter(Event event, HandlerPriority priority) { private void checkAndFilter(Event event, HandlerPriority priority) {
this.listeners.stream() // Create a collection of listeners.
List<EventHandler<? extends Event>> 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.handles().isInstance(event))
.filter(handler -> handler.getPriority() == priority) .filter(handler -> handler.getPriority() == priority)
// Invoke the event.
.toList().forEach(handler -> this.invokeHandler(event, handler)); .toList().forEach(handler -> this.invokeHandler(event, handler));
} }
/** /**
* Gets a plugin's instance by its name. * Gets a plugin's instance by its name.
*
* @param name The name of the plugin. * @param name The name of the plugin.
* @return Either null, or the plugin's instance. * @return Either null, or the plugin's instance.
*/ */
@Nullable
public Plugin getPlugin(String name) { public Plugin getPlugin(String name) {
return this.plugins.get(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. * 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. * @param handler The handler to invoke.
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T extends Event> void invokeHandler(Event event, EventHandler<T> handler) { private <T extends Event> void invokeHandler(Event event, EventHandler<T> handler) {
if(!event.isCanceled() || if (!event.isCanceled() ||
(event.isCanceled() && handler.ignoresCanceled()) (event.isCanceled() && handler.ignoresCanceled())
) handler.getCallback().consume((T) event); ) handler.getCallback().consume((T) event);
} }
......
package emu.grasscutter.server.event; package emu.grasscutter.server.event;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.plugin.Plugin;
import emu.grasscutter.utils.EventConsumer; import emu.grasscutter.utils.EventConsumer;
public final class EventHandler<T extends Event> { public final class EventHandler<T extends Event> {
...@@ -75,7 +76,7 @@ public final class EventHandler<T extends Event> { ...@@ -75,7 +76,7 @@ public final class EventHandler<T extends Event> {
/** /**
* Registers the handler into the PluginManager. * Registers the handler into the PluginManager.
*/ */
public void register() { public void register(Plugin plugin) {
Grasscutter.getPluginManager().registerListener(this); Grasscutter.getPluginManager().registerListener(plugin, this);
} }
} }
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment