diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index 980102957..ee5fe65c8 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 5335a2254..8246588ae 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; @@ -33,8 +34,9 @@ public final class Grasscutter { public static RunMode MODE = RunMode.BOTH; private static DispatchServer dispatchServer; private static GameServer gameServer; + private static PluginManager pluginManager; - public static final Reflections reflector = new Reflections("emu.grasscutter"); + public static final Reflections reflector = new Reflections(); static { // Declare logback configuration. @@ -52,15 +54,11 @@ public final class Grasscutter { for (String arg : args) { switch (arg.toLowerCase()) { - case "-auth": - MODE = RunMode.AUTH; - break; - case "-game": - MODE = RunMode.GAME; - break; - case "-handbook": - Tools.createGmHandbook(); - return; + case "-auth" -> MODE = RunMode.AUTH; + case "-game" -> MODE = RunMode.GAME; + case "-handbook" -> { + Tools.createGmHandbook(); return; + } } } @@ -71,19 +69,21 @@ public final class Grasscutter { ResourceLoader.loadAll(); // Database DatabaseManager.initialize(); + + // Create server instances. + dispatchServer = new DispatchServer(); + gameServer = new GameServer(new InetSocketAddress(getConfig().getGameServerOptions().Ip, getConfig().getGameServerOptions().Port)); + + // Create plugin manager instance. + pluginManager = new PluginManager(); // Start servers. if(getConfig().RunMode.equalsIgnoreCase("HYBRID")) { - dispatchServer = new DispatchServer(); dispatchServer.start(); - - gameServer = new GameServer(new InetSocketAddress(getConfig().getGameServerOptions().Ip, getConfig().getGameServerOptions().Port)); gameServer.start(); - } else if(getConfig().RunMode.equalsIgnoreCase("DISPATCH_ONLY")) { - dispatchServer = new DispatchServer(); + } else if (getConfig().RunMode.equalsIgnoreCase("DISPATCH_ONLY")) { dispatchServer.start(); - } else if(getConfig().RunMode.equalsIgnoreCase("GAME_ONLY")) { - gameServer = new GameServer(new InetSocketAddress(getConfig().getGameServerOptions().Ip, getConfig().getGameServerOptions().Port)); + } else if (getConfig().RunMode.equalsIgnoreCase("GAME_ONLY")) { gameServer.start(); } else { getLogger().error("Invalid server run mode. " + getConfig().RunMode); @@ -91,12 +91,23 @@ public final class Grasscutter { getLogger().error("Shutting down..."); System.exit(1); } - - + + // Enable all plugins. + pluginManager.enablePlugins(); // Open console. startConsole(); + // Hook into shutdown event. + Runtime.getRuntime().addShutdownHook(new Thread(Grasscutter::onShutdown)); } + + /** + * Server shutdown event. + */ + private static void onShutdown() { + // Disable all plugins. + pluginManager.disablePlugins(); + } public static void loadConfig() { try (FileReader file = new FileReader(configFile)) { @@ -112,7 +123,7 @@ public final class Grasscutter { try (FileWriter file = new FileWriter(configFile)) { file.write(gson.toJson(config)); } catch (Exception e) { - Grasscutter.getLogger().error("Config save error"); + Grasscutter.getLogger().error("Unable to save config file."); } } @@ -123,13 +134,13 @@ public final class Grasscutter { while ((input = br.readLine()) != null) { try { if(getConfig().RunMode.equalsIgnoreCase("DISPATCH_ONLY")) { - getLogger().error("Commands are not supported in dispatch only mode"); + getLogger().error("Commands are not supported in dispatch only mode."); return; } + CommandMap.getInstance().invoke(null, input); } catch (Exception e) { - Grasscutter.getLogger().error("Command error: "); - e.printStackTrace(); + Grasscutter.getLogger().error("Command error:", e); } } } catch (Exception e) { @@ -162,4 +173,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..a3160d7c7 --- /dev/null +++ b/src/main/java/emu/grasscutter/plugin/Plugin.java @@ -0,0 +1,67 @@ +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 PluginIdentifier identifier; + + /** + * This method is reflected into. + * + * Set plugin variables. + * @param identifier The plugin's identifier. + */ + private void initializePlugin(PluginIdentifier identifier) { + if(this.identifier == null) + this.identifier = identifier; + else Grasscutter.getLogger().warn(this.identifier.name + " had a reinitialization attempt."); + } + + /** + * 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..7b54f460f --- /dev/null +++ b/src/main/java/emu/grasscutter/plugin/PluginManager.java @@ -0,0 +1,152 @@ +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.Listener; +import emu.grasscutter.utils.Utils; +import org.reflections.Reflections; + +import java.io.File; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; + +/** + * Manages the server's plugins & the event system. + */ +public final class PluginManager { + private final Map plugins = new HashMap<>(); + private final Map> listeners = 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")) + .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().newInstance(); + this.loadPlugin(pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig)); + + 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, PluginIdentifier identifier) { + Grasscutter.getLogger().info("Loading plugin: " + identifier.name); + + // Add the plugin's identifier. + try { + Class pluginClass = Plugin.class; + Method method = pluginClass.getDeclaredMethod("initializePlugin", PluginIdentifier.class); + method.setAccessible(true); method.invoke(plugin, identifier); 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); + // Call the plugin's onLoad method. + plugin.onLoad(); + } + + /** + * Enables all registered plugins. + */ + public void enablePlugins() { + this.plugins.forEach((name, plugin) -> { + Grasscutter.getLogger().info("Enabling plugin: " + name); + plugin.onEnable(); + }); + } + + /** + * Disables all registered plugins. + */ + public void disablePlugins() { + this.plugins.forEach((name, plugin) -> { + Grasscutter.getLogger().info("Disabling plugin: " + name); + plugin.onDisable(); + }); + } + + /** + * Registers a plugin's event listener. + * @param plugin The plugin instance. + * @param listener The event listener. + */ + public void registerListener(Plugin plugin, Listener listener) { + this.listeners.computeIfAbsent(plugin, k -> new ArrayList<>()).add(listener); + } + + /** + * Invoke the provided event on all registered event listeners. + * @param event The event to invoke. + */ + public void invokeEvent(Event event) { + this.listeners.values().stream() + .flatMap(Collection::stream) + .forEach(listener -> this.invokeOnListener(listener, event)); + } + + /** + * Attempts to invoke the event on the provided listener. + */ + private void invokeOnListener(Listener listener, Event event) { + try { + Class listenerClass = listener.getClass(); + Method[] methods = listenerClass.getMethods(); + for (Method method : methods) { + if(!method.isAnnotationPresent(EventHandler.class)) return; + if(!method.getParameterTypes()[0].isAssignableFrom(event.getClass())) return; + method.invoke(listener, event); + } + } catch (Exception ignored) { } + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 6d8a06e7c..5cb06d5e1 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -18,6 +18,8 @@ import emu.grasscutter.net.proto.RegionInfoOuterClass.RegionInfo; import emu.grasscutter.net.proto.RegionSimpleInfoOuterClass.RegionSimpleInfo; import emu.grasscutter.server.dispatch.json.*; import emu.grasscutter.server.dispatch.json.ComboTokenReqJson.LoginTokenData; +import emu.grasscutter.server.event.dispatch.QueryAllRegionsEvent; +import emu.grasscutter.server.event.dispatch.QueryCurrentRegionEvent; import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.Utils; @@ -277,7 +279,11 @@ public final class DispatchServer { if (uri.getQuery() != null && uri.getQuery().length() > 0) { response = regionCurrentBase64; } - responseHTML(t, response); + + // Invoke event. + QueryCurrentRegionEvent event = new QueryCurrentRegionEvent(response); event.call(); + // Respond with event result. + responseHTML(t, event.getRegionInfo()); }); } diff --git a/src/main/java/emu/grasscutter/server/event/Cancellable.java b/src/main/java/emu/grasscutter/server/event/Cancellable.java new file mode 100644 index 000000000..0296f0b36 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/event/Cancellable.java @@ -0,0 +1,8 @@ +package emu.grasscutter.server.event; + +/** + * Implementing this interface marks an event as cancellable. + */ +public interface Cancellable { + void cancel(); +} diff --git a/src/main/java/emu/grasscutter/server/event/Event.java b/src/main/java/emu/grasscutter/server/event/Event.java new file mode 100644 index 000000000..bea7dd66f --- /dev/null +++ b/src/main/java/emu/grasscutter/server/event/Event.java @@ -0,0 +1,32 @@ +package emu.grasscutter.server.event; + +import emu.grasscutter.Grasscutter; + +/** + * A generic server event. + */ +public abstract class Event { + private boolean cancelled = false; + + /** + * Return the cancelled state of the event. + */ + public boolean isCanceled() { + return this.cancelled; + } + + /** + * Cancels the event if possible. + */ + public void cancel() { + if(this instanceof Cancellable) + this.cancelled = true; + } + + /** + * Pushes this event to all listeners. + */ + public void call() { + Grasscutter.getPluginManager().invokeEvent(this); + } +} diff --git a/src/main/java/emu/grasscutter/server/event/EventHandler.java b/src/main/java/emu/grasscutter/server/event/EventHandler.java new file mode 100644 index 000000000..d924933f2 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/event/EventHandler.java @@ -0,0 +1,11 @@ +package emu.grasscutter.server.event; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Declares a class as an event listener/handler. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface EventHandler { +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/event/Listener.java b/src/main/java/emu/grasscutter/server/event/Listener.java new file mode 100644 index 000000000..2949cfe4a --- /dev/null +++ b/src/main/java/emu/grasscutter/server/event/Listener.java @@ -0,0 +1,7 @@ +package emu.grasscutter.server.event; + +/** + * Implementing this interface declares a class as an event listener. + */ +public interface Listener { +} diff --git a/src/main/java/emu/grasscutter/server/event/ServerEvent.java b/src/main/java/emu/grasscutter/server/event/ServerEvent.java new file mode 100644 index 000000000..e87abae0d --- /dev/null +++ b/src/main/java/emu/grasscutter/server/event/ServerEvent.java @@ -0,0 +1,17 @@ +package emu.grasscutter.server.event; + +/** + * An event that is related to the internals of the server. + */ +public abstract class ServerEvent extends Event { + protected final Type type; + + public ServerEvent(Type type) { + this.type = type; + } + + public enum Type { + DISPATCH, + GAME + } +} diff --git a/src/main/java/emu/grasscutter/server/event/dispatch/QueryAllRegionsEvent.java b/src/main/java/emu/grasscutter/server/event/dispatch/QueryAllRegionsEvent.java new file mode 100644 index 000000000..8595f6221 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/event/dispatch/QueryAllRegionsEvent.java @@ -0,0 +1,21 @@ +package emu.grasscutter.server.event.dispatch; + +import emu.grasscutter.server.event.ServerEvent; + +public final class QueryAllRegionsEvent extends ServerEvent { + private String regionList; + + public QueryAllRegionsEvent(String regionList) { + super(Type.DISPATCH); + + this.regionList = regionList; + } + + public void setRegionList(String regionList) { + this.regionList = regionList; + } + + public String getRegionList() { + return this.regionList; + } +} diff --git a/src/main/java/emu/grasscutter/server/event/dispatch/QueryCurrentRegionEvent.java b/src/main/java/emu/grasscutter/server/event/dispatch/QueryCurrentRegionEvent.java new file mode 100644 index 000000000..d6a20b2df --- /dev/null +++ b/src/main/java/emu/grasscutter/server/event/dispatch/QueryCurrentRegionEvent.java @@ -0,0 +1,21 @@ +package emu.grasscutter.server.event.dispatch; + +import emu.grasscutter.server.event.ServerEvent; + +public final class QueryCurrentRegionEvent extends ServerEvent { + private String regionInfo; + + public QueryCurrentRegionEvent(String regionInfo) { + super(Type.DISPATCH); + + this.regionInfo = regionInfo; + } + + public void setRegionInfo(String regionInfo) { + this.regionInfo = regionInfo; + } + + public String getRegionInfo() { + return this.regionInfo; + } +} diff --git a/src/main/java/emu/grasscutter/server/event/game/ReceivePacketEvent.java b/src/main/java/emu/grasscutter/server/event/game/ReceivePacketEvent.java new file mode 100644 index 000000000..51109c720 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/event/game/ReceivePacketEvent.java @@ -0,0 +1,35 @@ +package emu.grasscutter.server.event.game; + +import emu.grasscutter.server.event.Cancellable; +import emu.grasscutter.server.event.ServerEvent; +import emu.grasscutter.server.game.GameSession; + +public final class ReceivePacketEvent extends ServerEvent implements Cancellable { + private final GameSession gameSession; + private final int packetId; + private byte[] packetData; + + public ReceivePacketEvent(GameSession gameSession, int packetId, byte[] packetData) { + super(Type.GAME); + + this.gameSession = gameSession; + this.packetId = packetId; + this.packetData = packetData; + } + + public GameSession getGameSession() { + return this.gameSession; + } + + public int getPacketId() { + return this.packetId; + } + + public void setPacketData(byte[] packetData) { + this.packetData = packetData; + } + + public byte[] getPacketData() { + return this.packetData; + } +} diff --git a/src/main/java/emu/grasscutter/server/event/game/SendPacketEvent.java b/src/main/java/emu/grasscutter/server/event/game/SendPacketEvent.java new file mode 100644 index 000000000..7a25b4e10 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/event/game/SendPacketEvent.java @@ -0,0 +1,30 @@ +package emu.grasscutter.server.event.game; + +import emu.grasscutter.net.packet.GenshinPacket; +import emu.grasscutter.server.event.Cancellable; +import emu.grasscutter.server.event.ServerEvent; +import emu.grasscutter.server.game.GameSession; + +public final class SendPacketEvent extends ServerEvent implements Cancellable { + private final GameSession gameSession; + private GenshinPacket packet; + + public SendPacketEvent(GameSession gameSession, GenshinPacket packet) { + super(Type.GAME); + + this.gameSession = gameSession; + this.packet = packet; + } + + public GameSession getGameSession() { + return this.gameSession; + } + + public void setPacket(GenshinPacket packet) { + this.packet = packet; + } + + public GenshinPacket getPacket() { + return this.packet; + } +} diff --git a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java index 62a57df91..50d508bed 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java +++ b/src/main/java/emu/grasscutter/server/game/GameServerPacketHandler.java @@ -2,6 +2,7 @@ package emu.grasscutter.server.game; import java.util.Set; +import emu.grasscutter.server.event.game.ReceivePacketEvent; import org.reflections.Reflections; import emu.grasscutter.Grasscutter; @@ -48,9 +49,7 @@ public class GameServerPacketHandler { } public void handle(GameSession session, int opcode, byte[] header, byte[] payload) { - PacketHandler handler = null; - - handler = this.handlers.get(opcode); + PacketHandler handler = this.handlers.get(opcode); if (handler != null) { try { @@ -77,8 +76,10 @@ public class GameServerPacketHandler { } } - // Handle - handler.handle(session, header, payload); + // Invoke event. + ReceivePacketEvent event = new ReceivePacketEvent(session, opcode, payload); event.call(); + if(!event.isCanceled()) // If event is not canceled, continue. + handler.handle(session, header, event.getPacketData()); } catch (Exception ex) { // TODO Remove this when no more needed ex.printStackTrace(); diff --git a/src/main/java/emu/grasscutter/server/game/GameSession.java b/src/main/java/emu/grasscutter/server/game/GameSession.java index ebd66dc20..53b4f32cc 100644 --- a/src/main/java/emu/grasscutter/server/game/GameSession.java +++ b/src/main/java/emu/grasscutter/server/game/GameSession.java @@ -10,6 +10,7 @@ import emu.grasscutter.game.GenshinPlayer; import emu.grasscutter.net.packet.GenshinPacket; import emu.grasscutter.net.packet.PacketOpcodesUtil; import emu.grasscutter.netty.MihoyoKcpChannel; +import emu.grasscutter.server.event.game.SendPacketEvent; import emu.grasscutter.utils.Crypto; import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.Utils; @@ -161,16 +162,15 @@ public class GameSession extends MihoyoKcpChannel { genshinPacket.buildHeader(this.getNextClientSequence()); } - // Build packet - byte[] data = genshinPacket.build(); - // Log if (Grasscutter.getConfig().getGameServerOptions().LOG_PACKETS) { logPacket(genshinPacket); } - - // Send - send(data); + + // Invoke event. + SendPacketEvent event = new SendPacketEvent(this, genshinPacket); event.call(); + if(!event.isCanceled()) // If event is not cancelled, continue. + this.send(event.getPacket().build()); } private void logPacket(int opcode) { 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. */ diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/resources/META-INF/MANIFEST.MF similarity index 100% rename from src/main/java/META-INF/MANIFEST.MF rename to src/main/resources/META-INF/MANIFEST.MF