Implement a plugin API for developers

Add a developer-friendly API to Grasscutter
This commit is contained in:
Magix 2022-04-30 16:12:39 -04:00 committed by GitHub
commit 0e55a0ce0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 394 additions and 126 deletions

View File

@ -37,7 +37,7 @@ sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
group = 'tech.xigam' group = 'tech.xigam'
version = '1.0.0-dev' version = '1.0.2-dev'
sourceCompatibility = 17 sourceCompatibility = 17
targetCompatibility = 17 targetCompatibility = 17

View File

@ -8,13 +8,14 @@ import java.util.List;
@Command(label = "coop", usage = "coop", @Command(label = "coop", usage = "coop",
description = "Forces someone to join the world of others", permission = "server.coop") description = "Forces someone to join the world of others", permission = "server.coop")
public class CoopCommand implements CommandHandler { public final class CoopCommand implements CommandHandler {
@Override @Override
public void execute(Player sender, List<String> args) { public void execute(Player sender, List<String> args) {
if (args.size() < 2) { if (args.size() < 2) {
CommandHandler.sendMessage(sender, "Usage: coop <playerId> <target playerId>"); CommandHandler.sendMessage(sender, "Usage: coop <playerId> <target playerId>");
return; return;
} }
try { try {
int tid = Integer.parseInt(args.get(0)); int tid = Integer.parseInt(args.get(0));
int hostId = Integer.parseInt(args.get(1)); int hostId = Integer.parseInt(args.get(1));

View File

@ -15,7 +15,7 @@ import java.util.*;
@Command(label = "giveall", usage = "giveall [player] [amount]", @Command(label = "giveall", usage = "giveall [player] [amount]",
description = "Gives all items", aliases = {"givea"}, permission = "player.giveall", threading = true) description = "Gives all items", aliases = {"givea"}, permission = "player.giveall", threading = true)
public class GiveAllCommand implements CommandHandler { public final class GiveAllCommand implements CommandHandler {
@Override @Override
public void execute(Player sender, List<String> args) { public void execute(Player sender, List<String> args) {
@ -142,16 +142,11 @@ public class GiveAllCommand implements CommandHandler {
} }
} }
if (testItemsList.contains(itemId)) { return testItemsList.contains(itemId);
return true;
}
return false;
} }
static class Range { static class Range {
private int min; private final int min, max;
private int max;
public Range(int min, int max) { public Range(int min, int max) {
if(min > max){ if(min > max){
@ -159,6 +154,7 @@ public class GiveAllCommand implements CommandHandler {
max ^= min; max ^= min;
min ^= max; min ^= max;
} }
this.min = min; this.min = min;
this.max = max; this.max = max;
} }

View File

@ -6,24 +6,20 @@ import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.mail.Mail;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.packet.send.PacketMailChangeNotify;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
@Command(label = "sendmail", usage = "sendmail <userId|all|help> [templateId]", @Command(label = "sendmail", usage = "sendmail <userId|all|help> [templateId]",
description = "Sends mail to the specified user. The usage of this command changes based on it's composition state.", permission = "server.sendmail") description = "Sends mail to the specified user. The usage of this command changes based on it's composition state.", permission = "server.sendmail")
public class SendMailCommand implements CommandHandler { public final class SendMailCommand implements CommandHandler {
// TODO: You should be able to do /sendmail and then just send subsequent messages until you finish // TODO: You should be able to do /sendmail and then just send subsequent messages until you finish
// However, due to the current nature of the command system, I don't think this is possible without rewriting // However, due to the current nature of the command system, I don't think this is possible without rewriting
// the command system (again). For now this will do // the command system (again). For now this will do
// Key = User that is constructing the mail. // Key = User that is constructing the mail.
private static HashMap<Integer, MailBuilder> mailBeingConstructed = new HashMap<Integer, MailBuilder>(); private static final HashMap<Integer, MailBuilder> mailBeingConstructed = new HashMap<Integer, MailBuilder>();
// Yes this is awful and I hate it. // Yes this is awful and I hate it.
@Override @Override
@ -48,7 +44,6 @@ public class SendMailCommand implements CommandHandler {
default -> { default -> {
if (DatabaseHelper.getPlayerById(Integer.parseInt(args.get(0))) != null) { if (DatabaseHelper.getPlayerById(Integer.parseInt(args.get(0))) != null) {
mailBuilder = new MailBuilder(Integer.parseInt(args.get(0)), new Mail()); mailBuilder = new MailBuilder(Integer.parseInt(args.get(0)), new Mail());
break;
} else { } else {
CommandHandler.sendMessage(sender, "The user with an id of '" + args.get(0) + "' does not exist"); CommandHandler.sendMessage(sender, "The user with an id of '" + args.get(0) + "' does not exist");
return; return;
@ -73,7 +68,7 @@ public class SendMailCommand implements CommandHandler {
} }
case "finish" -> { case "finish" -> {
if (mailBuilder.constructionStage == 3) { if (mailBuilder.constructionStage == 3) {
if (mailBuilder.sendToAll == false) { if (!mailBuilder.sendToAll) {
Grasscutter.getGameServer().getPlayerByUid(mailBuilder.recipient, true).sendMail(mailBuilder.mail); Grasscutter.getGameServer().getPlayerByUid(mailBuilder.recipient, true).sendMail(mailBuilder.mail);
CommandHandler.sendMessage(sender, "Message sent to user " + mailBuilder.recipient + "!"); CommandHandler.sendMessage(sender, "Message sent to user " + mailBuilder.recipient + "!");
} else { } else {

View File

@ -9,23 +9,25 @@ import java.util.List;
@Command(label = "tpall", usage = "tpall", @Command(label = "tpall", usage = "tpall",
description = "Teleports all players in your world to your position", permission = "player.tpall") description = "Teleports all players in your world to your position", permission = "player.tpall")
public class TpallCommand implements CommandHandler { public final class TeleportAllCommand implements CommandHandler {
@Override @Override
public void execute(Player sender, List<String> args) { public void execute(Player sender, List<String> args) {
if (sender == null) { if (sender == null) {
CommandHandler.sendMessage(null, "Run this command in-game."); CommandHandler.sendMessage(null, "Run this command in-game.");
return; return;
} }
if (!sender.getWorld().isMultiplayer()) { if (!sender.getWorld().isMultiplayer()) {
CommandHandler.sendMessage(sender, "You only can use this command in MP mode."); CommandHandler.sendMessage(sender, "You only can use this command in MP mode.");
return; return;
} }
for (Player gp : sender.getWorld().getPlayers()) {
if (gp.equals(sender)) for (Player player : sender.getWorld().getPlayers()) {
if (player.equals(sender))
continue; continue;
Position pos = sender.getPos(); Position pos = sender.getPos();
gp.getWorld().transferPlayerToScene(gp, sender.getSceneId(), pos); player.getWorld().transferPlayerToScene(player, sender.getSceneId(), pos);
} }
} }
} }

View File

@ -37,6 +37,9 @@ import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass;
import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture; import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture;
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
import emu.grasscutter.net.proto.SocialShowAvatarInfoOuterClass; import emu.grasscutter.net.proto.SocialShowAvatarInfoOuterClass;
import emu.grasscutter.server.event.player.PlayerJoinEvent;
import emu.grasscutter.server.event.player.PlayerQuitEvent;
import emu.grasscutter.server.event.player.PlayerReceiveMailEvent;
import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
@ -725,9 +728,13 @@ public class Player {
public List<Mail> getAllMail() { return this.mail; } public List<Mail> getAllMail() { return this.mail; }
public void sendMail(Mail message) { public void sendMail(Mail message) {
// Call mail receive event.
PlayerReceiveMailEvent event = new PlayerReceiveMailEvent(this, message); event.call();
if(event.isCanceled()) return; message = event.getMessage();
this.mail.add(message); this.mail.add(message);
this.save(); this.save();
Grasscutter.getLogger().info("Mail sent to user [" + this.getUid() + ":" + this.getNickname() + "]!"); Grasscutter.getLogger().debug("Mail sent to user [" + this.getUid() + ":" + this.getNickname() + "]!");
if(this.isOnline()) { if(this.isOnline()) {
this.sendPacket(new PacketMailChangeNotify(this, message)); this.sendPacket(new PacketMailChangeNotify(this, message));
} // TODO: setup a way for the mail notification to show up when someone receives mail when they were offline } // TODO: setup a way for the mail notification to show up when someone receives mail when they were offline
@ -1037,6 +1044,11 @@ public class Player {
// First notify packets sent // First notify packets sent
this.setHasSentAvatarDataNotify(true); this.setHasSentAvatarDataNotify(true);
// Call join event.
PlayerJoinEvent event = new PlayerJoinEvent(this); event.call();
if(event.isCanceled()) // If event is not cancelled, continue.
session.close();
} }
public void onLogout() { public void onLogout() {
@ -1055,6 +1067,9 @@ public class Player {
this.save(); this.save();
this.getTeamManager().saveAvatars(); this.getTeamManager().saveAvatars();
this.getFriendsList().save(); this.getFriendsList().save();
// Call quit event.
PlayerQuitEvent event = new PlayerQuitEvent(this); event.call();
} }
public enum SceneLoadState { public enum SceneLoadState {

View File

@ -1,13 +1,22 @@
package emu.grasscutter.plugin; package emu.grasscutter.plugin;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.plugin.api.ServerHook;
import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServer;
import java.io.File;
import java.io.InputStream;
import java.net.URLClassLoader;
/** /**
* The base class for all plugins to extend. * The base class for all plugins to extend.
*/ */
public abstract class Plugin { public abstract class Plugin {
private final ServerHook server = ServerHook.getInstance();
private PluginIdentifier identifier; private PluginIdentifier identifier;
private URLClassLoader classLoader;
private File dataFolder;
/** /**
* This method is reflected into. * This method is reflected into.
@ -15,10 +24,20 @@ public abstract class Plugin {
* Set plugin variables. * Set plugin variables.
* @param identifier The plugin's identifier. * @param identifier The plugin's identifier.
*/ */
private void initializePlugin(PluginIdentifier identifier) { private void initializePlugin(PluginIdentifier identifier, URLClassLoader classLoader) {
if(this.identifier == null) if(this.identifier != null) {
Grasscutter.getLogger().warn(this.identifier.name + " had a reinitialization attempt.");
return;
}
this.identifier = identifier; this.identifier = identifier;
else Grasscutter.getLogger().warn(this.identifier.name + " had a reinitialization attempt."); this.classLoader = classLoader;
this.dataFolder = new File(Grasscutter.getConfig().PLUGINS_FOLDER, identifier.name);
if(!this.dataFolder.exists() && !this.dataFolder.mkdirs()) {
Grasscutter.getLogger().warn("Failed to create plugin data folder for " + this.identifier.name);
return;
}
} }
/** /**
@ -55,7 +74,32 @@ public abstract class Plugin {
* @return A server instance. * @return A server instance.
*/ */
public final GameServer getServer() { public final GameServer getServer() {
return Grasscutter.getGameServer(); return this.server.getGameServer();
}
/**
* Returns an input stream for a resource in the JAR file.
* @param resourceName The name of the resource.
* @return An input stream.
*/
public final InputStream getResource(String resourceName) {
return this.classLoader.getResourceAsStream(resourceName);
}
/**
* Returns a directory where plugins can store data files.
* @return A directory on the file system.
*/
public final File getDataFolder() {
return this.dataFolder;
}
/**
* Returns the server hook.
* @return A server hook singleton.
*/
public final ServerHook getHandle() {
return this.server;
} }
/* Called when the plugin is first loaded. */ /* Called when the plugin is first loaded. */

View File

@ -3,9 +3,9 @@ package emu.grasscutter.plugin;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.event.Event; import emu.grasscutter.server.event.Event;
import emu.grasscutter.server.event.EventHandler; import emu.grasscutter.server.event.EventHandler;
import emu.grasscutter.server.event.Listener; import emu.grasscutter.server.event.HandlerPriority;
import emu.grasscutter.utils.EventConsumer;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import org.reflections.Reflections;
import java.io.File; import java.io.File;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -21,7 +21,7 @@ import java.util.jar.JarFile;
*/ */
public final class PluginManager { public final class PluginManager {
private final Map<String, Plugin> plugins = new HashMap<>(); private final Map<String, Plugin> plugins = new HashMap<>();
private final Map<Plugin, List<Listener>> listeners = new HashMap<>(); private final List<EventHandler<? extends Event>> listeners = new LinkedList<>();
public PluginManager() { public PluginManager() {
this.loadPlugins(); // Load all plugins from the plugins directory. this.loadPlugins(); // Load all plugins from the plugins directory.
@ -68,12 +68,12 @@ public final class PluginManager {
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("/", ".");
Class<?> clazz = loader.loadClass(className); loader.loadClass(className);
} }
Class<?> pluginClass = loader.loadClass(pluginConfig.mainClass); Class<?> pluginClass = loader.loadClass(pluginConfig.mainClass);
Plugin pluginInstance = (Plugin) pluginClass.getDeclaredConstructor().newInstance(); Plugin pluginInstance = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
this.loadPlugin(pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig)); this.loadPlugin(pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig), loader);
fileReader.close(); // Close the file reader. fileReader.close(); // Close the file reader.
} catch (ClassNotFoundException ignored) { } catch (ClassNotFoundException ignored) {
@ -89,14 +89,14 @@ public final class PluginManager {
* 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) { private void loadPlugin(Plugin plugin, PluginIdentifier identifier, URLClassLoader classLoader) {
Grasscutter.getLogger().info("Loading plugin: " + identifier.name); Grasscutter.getLogger().info("Loading plugin: " + identifier.name);
// Add the plugin's identifier. // Add the plugin's identifier.
try { try {
Class<Plugin> pluginClass = Plugin.class; Class<Plugin> pluginClass = Plugin.class;
Method method = pluginClass.getDeclaredMethod("initializePlugin", PluginIdentifier.class); Method method = pluginClass.getDeclaredMethod("initializePlugin", PluginIdentifier.class, URLClassLoader.class);
method.setAccessible(true); method.invoke(plugin, identifier); 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);
} }
@ -129,11 +129,10 @@ public final class PluginManager {
/** /**
* Registers a plugin's event listener. * Registers a plugin's event listener.
* @param plugin The plugin instance.
* @param listener The event listener. * @param listener The event listener.
*/ */
public void registerListener(Plugin plugin, Listener listener) { public void registerListener(EventHandler<? extends Event> listener) {
this.listeners.computeIfAbsent(plugin, k -> new ArrayList<>()).add(listener); this.listeners.add(listener);
} }
/** /**
@ -141,23 +140,31 @@ public final class PluginManager {
* @param event The event to invoke. * @param event The event to invoke.
*/ */
public void invokeEvent(Event event) { public void invokeEvent(Event event) {
this.listeners.values().stream() EnumSet.allOf(HandlerPriority.class)
.flatMap(Collection::stream) .forEach(priority -> this.checkAndFilter(event, priority));
.forEach(listener -> this.invokeOnListener(listener, event));
} }
/** /**
* Attempts to invoke the event on the provided listener. * Check an event to handlers for the priority.
* @param event The event being called.
* @param priority The priority to call for.
*/ */
private void invokeOnListener(Listener listener, Event event) { private void checkAndFilter(Event event, HandlerPriority priority) {
try { this.listeners.stream()
Class<?> listenerClass = listener.getClass(); .filter(handler -> handler.handles().isInstance(event))
Method[] methods = listenerClass.getMethods(); .filter(handler -> handler.getPriority() == priority)
for (Method method : methods) { .toList().forEach(handler -> this.invokeHandler(event, handler));
if(!method.isAnnotationPresent(EventHandler.class)) return;
if(!method.getParameterTypes()[0].isAssignableFrom(event.getClass())) return;
method.invoke(listener, event);
} }
} catch (Exception ignored) { }
/**
* Performs logic checks then invokes the provided event handler.
* @param event The event passed through to the handler.
* @param handler The handler to invoke.
*/
@SuppressWarnings("unchecked")
private <T extends Event> void invokeHandler(Event event, EventHandler<T> handler) {
if(!event.isCanceled() ||
(event.isCanceled() && handler.ignoresCanceled())
) handler.getCallback().consume((T) event);
} }
} }

View File

@ -1,6 +1,9 @@
package emu.grasscutter.plugin.api; package emu.grasscutter.plugin.api;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.dispatch.DispatchServer;
import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameServer;
import java.util.LinkedList; import java.util.LinkedList;
@ -11,7 +14,8 @@ import java.util.List;
*/ */
public final class ServerHook { public final class ServerHook {
private static ServerHook instance; private static ServerHook instance;
private final GameServer server; private final GameServer gameServer;
private final DispatchServer dispatchServer;
/** /**
* Gets the server hook instance. * Gets the server hook instance.
@ -23,19 +27,47 @@ public final class ServerHook {
/** /**
* Hooks into a server. * Hooks into a server.
* @param server The server to hook into. * @param gameServer The game server to hook into.
* @param dispatchServer The dispatch server to hook into.
*/ */
public ServerHook(GameServer server) { public ServerHook(GameServer gameServer, DispatchServer dispatchServer) {
this.server = server; this.gameServer = gameServer;
this.dispatchServer = dispatchServer;
instance = this; instance = this;
} }
/**
* @return The game server.
*/
public GameServer getGameServer() {
return this.gameServer;
}
/**
* @return The dispatch server.
*/
public DispatchServer getDispatchServer() {
return this.dispatchServer;
}
/** /**
* Gets all online players. * Gets all online players.
* @return Players connected to the server. * @return Players connected to the server.
*/ */
public List<Player> getOnlinePlayers() { public List<Player> getOnlinePlayers() {
return new LinkedList<>(this.server.getPlayers().values()); return new LinkedList<>(this.gameServer.getPlayers().values());
}
/**
* Registers a command to the {@link emu.grasscutter.command.CommandMap}.
* @param handler The command handler.
*/
public void registerCommand(CommandHandler handler) {
Class<? extends CommandHandler> clazz = handler.getClass();
if(!clazz.isAnnotationPresent(Command.class))
throw new IllegalArgumentException("Command handler must be annotated with @Command.");
Command commandData = clazz.getAnnotation(Command.class);
this.gameServer.getCommandMap().registerCommand(commandData.label(), handler);
} }
} }

View File

@ -30,6 +30,7 @@ import java.net.BindException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.URI; import java.net.URI;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore; import java.security.KeyStore;
import java.util.*; import java.util.*;
@ -209,7 +210,7 @@ public final class DispatchServer {
return null; return null;
} }
private KeyManagerFactory createKeyManagerFactory(File keystore, String password) throws Exception { private KeyManagerFactory createKeyManagerFactory(File keystore, String password) {
char[] pass = password.toCharArray(); char[] pass = password.toCharArray();
KeyManagerFactory kmf = null; KeyManagerFactory kmf = null;
@ -220,8 +221,8 @@ public final class DispatchServer {
kmf = KeyManagerFactory.getInstance("SunX509"); kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, pass); kmf.init(ks, pass);
} catch (Exception e) { } catch (Exception exception) {
throw e; Grasscutter.getLogger().error("Unable to load keystore.", exception);
} }
return kmf; return kmf;
@ -243,10 +244,9 @@ public final class DispatchServer {
try { try {
kmf = createKeyManagerFactory(keystoreFile, "123456"); kmf = createKeyManagerFactory(keystoreFile, "123456");
Grasscutter.getLogger().warn( Grasscutter.getLogger().warn(
"[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to 123456 in config.json."); "[Dispatch] The default keystore password was loaded successfully. Please consider setting the password to '123456' in config.json.");
} catch (Exception e2) { } catch (Exception e2) {
Grasscutter.getLogger().warn("[Dispatch] Error while loading keystore!"); Grasscutter.getLogger().warn("[Dispatch] Error while loading keystore!", e2);
e2.printStackTrace();
} }
} }
} }
@ -257,7 +257,7 @@ public final class DispatchServer {
server = this.safelyCreateServer(this.getAddress()); server = this.safelyCreateServer(this.getAddress());
} }
HttpsServer httpsServer = null; HttpsServer httpsServer;
try { try {
httpsServer = HttpsServer.create(getAddress(), 0); httpsServer = HttpsServer.create(getAddress(), 0);
@ -339,10 +339,6 @@ public final class DispatchServer {
// added. // added.
account = DatabaseHelper.createAccountWithId(requestData.account, 0); account = DatabaseHelper.createAccountWithId(requestData.account, 0);
for (String permission : Grasscutter.getConfig().getDispatchOptions().defaultPermissions) {
account.addPermission(permission);
}
if (account != null) { if (account != null) {
responseData.message = "OK"; responseData.message = "OK";
responseData.data.account.uid = account.getId(); responseData.data.account.uid = account.getId();
@ -352,6 +348,9 @@ public final class DispatchServer {
Grasscutter.getLogger() Grasscutter.getLogger()
.info(String.format("[Dispatch] Client %s failed to log in: Account %s created", .info(String.format("[Dispatch] Client %s failed to log in: Account %s created",
t.getRemoteAddress(), responseData.data.account.uid)); t.getRemoteAddress(), responseData.data.account.uid));
for (String permission : Grasscutter.getConfig().getDispatchOptions().defaultPermissions) {
account.addPermission(permission);
}
} else { } else {
responseData.retcode = -201; responseData.retcode = -201;
responseData.message = "Username not found, create failed."; responseData.message = "Username not found, create failed.";
@ -575,15 +574,11 @@ public final class DispatchServer {
if (next > last) { if (next > last) {
int eqPos = qs.indexOf('=', last); int eqPos = qs.indexOf('=', last);
try {
if (eqPos < 0 || eqPos > next) { if (eqPos < 0 || eqPos > next) {
result.put(URLDecoder.decode(qs.substring(last, next), "utf-8"), ""); result.put(URLDecoder.decode(qs.substring(last, next), StandardCharsets.UTF_8), "");
} else { } else {
result.put(URLDecoder.decode(qs.substring(last, eqPos), "utf-8"), result.put(URLDecoder.decode(qs.substring(last, eqPos), StandardCharsets.UTF_8),
URLDecoder.decode(qs.substring(eqPos + 1, next), "utf-8")); URLDecoder.decode(qs.substring(eqPos + 1, next), StandardCharsets.UTF_8));
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // will never happen, utf-8 support is mandatory for java
} }
} }
last = next + 1; last = next + 1;

View File

@ -1,11 +1,81 @@
package emu.grasscutter.server.event; package emu.grasscutter.server.event;
import java.lang.annotation.Retention; import emu.grasscutter.Grasscutter;
import java.lang.annotation.RetentionPolicy; import emu.grasscutter.utils.EventConsumer;
/** public final class EventHandler<T extends Event> {
* Declares a class as an event listener/handler. private final Class<T> eventClass;
private EventConsumer<T> listener;
private HandlerPriority priority;
private boolean handleCanceled;
public EventHandler(Class<T> eventClass) {
this.eventClass = eventClass;
}
/**
* Gets which event this handler is handling.
* @return An event class.
*/ */
@Retention(RetentionPolicy.RUNTIME) public Class<T> handles() {
public @interface EventHandler { return this.eventClass;
}
/**
* Returns the callback for the handler.
* @return A consumer callback.
*/
public EventConsumer<T> getCallback() {
return this.listener;
}
/**
* Returns the handler's priority.
* @return The priority of the handler.
*/
public HandlerPriority getPriority() {
return this.priority;
}
/**
* Returns if the handler will ignore cancelled events.
* @return The ignore cancelled state.
*/
public boolean ignoresCanceled() {
return this.handleCanceled;
}
/**
* Sets the callback method for when the event is invoked.
* @param listener An event handler method.
* @return Method chaining.
*/
public EventHandler<T> listener(EventConsumer<T> listener) {
this.listener = listener; return this;
}
/**
* Changes the handler's priority in handling events.
* @param priority The priority of the handler.
* @return Method chaining.
*/
public EventHandler<T> priority(HandlerPriority priority) {
this.priority = priority; return this;
}
/**
* Sets if the handler will ignore cancelled events.
* @param ignore If the handler should ignore cancelled events.
* @return Method chaining.
*/
public EventHandler<T> ignore(boolean ignore) {
this.handleCanceled = ignore; return this;
}
/**
* Registers the handler into the PluginManager.
*/
public void register() {
Grasscutter.getPluginManager().registerListener(this);
}
} }

View File

@ -0,0 +1,18 @@
package emu.grasscutter.server.event;
public enum HandlerPriority {
/**
* The handler will be called before every other handler.
*/
HIGH,
/**
* The handler will be called the same time as other handlers.
*/
NORMAL,
/**
* The handler will be called after every other handler.
*/
LOW
}

View File

@ -1,7 +0,0 @@
package emu.grasscutter.server.event;
/**
* Implementing this interface declares a class as an event listener.
*/
public interface Listener {
}

View File

@ -1,6 +1,6 @@
package emu.grasscutter.server.event.dispatch; package emu.grasscutter.server.event.dispatch;
import emu.grasscutter.server.event.ServerEvent; import emu.grasscutter.server.event.types.ServerEvent;
public final class QueryAllRegionsEvent extends ServerEvent { public final class QueryAllRegionsEvent extends ServerEvent {
private String regionList; private String regionList;

View File

@ -1,6 +1,6 @@
package emu.grasscutter.server.event.dispatch; package emu.grasscutter.server.event.dispatch;
import emu.grasscutter.server.event.ServerEvent; import emu.grasscutter.server.event.types.ServerEvent;
public final class QueryCurrentRegionEvent extends ServerEvent { public final class QueryCurrentRegionEvent extends ServerEvent {
private String regionInfo; private String regionInfo;

View File

@ -0,0 +1,27 @@
package emu.grasscutter.server.event.game;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.event.types.GameEvent;
import emu.grasscutter.server.game.GameSession;
public final class PlayerCreationEvent extends GameEvent {
private final GameSession session;
private Class<? extends Player> playerClass;
public PlayerCreationEvent(GameSession session, Class<? extends Player> playerClass) {
this.session = session;
this.playerClass = playerClass;
}
public GameSession getSession() {
return this.session;
}
public void setPlayerClass(Class<? extends Player> playerClass) {
this.playerClass = playerClass;
}
public Class<? extends Player> getPlayerClass() {
return this.playerClass;
}
}

View File

@ -1,7 +1,7 @@
package emu.grasscutter.server.event.game; package emu.grasscutter.server.event.game;
import emu.grasscutter.server.event.Cancellable; import emu.grasscutter.server.event.Cancellable;
import emu.grasscutter.server.event.ServerEvent; import emu.grasscutter.server.event.types.ServerEvent;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
public final class ReceivePacketEvent extends ServerEvent implements Cancellable { public final class ReceivePacketEvent extends ServerEvent implements Cancellable {

View File

@ -2,7 +2,7 @@ package emu.grasscutter.server.event.game;
import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.BasePacket;
import emu.grasscutter.server.event.Cancellable; import emu.grasscutter.server.event.Cancellable;
import emu.grasscutter.server.event.ServerEvent; import emu.grasscutter.server.event.types.ServerEvent;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
public final class SendPacketEvent extends ServerEvent implements Cancellable { public final class SendPacketEvent extends ServerEvent implements Cancellable {

View File

@ -1,6 +1,6 @@
package emu.grasscutter.server.event.game; package emu.grasscutter.server.event.game;
import emu.grasscutter.server.event.ServerEvent; import emu.grasscutter.server.event.types.ServerEvent;
public final class ServerTickEvent extends ServerEvent { public final class ServerTickEvent extends ServerEvent {
public ServerTickEvent() { public ServerTickEvent() {

View File

@ -1,6 +1,6 @@
package emu.grasscutter.server.event.internal; package emu.grasscutter.server.event.internal;
import emu.grasscutter.server.event.ServerEvent; import emu.grasscutter.server.event.types.ServerEvent;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;

View File

@ -1,6 +1,6 @@
package emu.grasscutter.server.event.internal; package emu.grasscutter.server.event.internal;
import emu.grasscutter.server.event.ServerEvent; import emu.grasscutter.server.event.types.ServerEvent;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;

View File

@ -0,0 +1,11 @@
package emu.grasscutter.server.event.player;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.event.Cancellable;
import emu.grasscutter.server.event.types.PlayerEvent;
public final class PlayerJoinEvent extends PlayerEvent implements Cancellable {
public PlayerJoinEvent(Player player) {
super(player);
}
}

View File

@ -0,0 +1,11 @@
package emu.grasscutter.server.event.player;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.event.types.GameEvent;
import emu.grasscutter.server.event.types.PlayerEvent;
public final class PlayerQuitEvent extends PlayerEvent {
public PlayerQuitEvent(Player player) {
super(player);
}
}

View File

@ -0,0 +1,24 @@
package emu.grasscutter.server.event.player;
import emu.grasscutter.game.mail.Mail;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.event.Cancellable;
import emu.grasscutter.server.event.types.PlayerEvent;
public final class PlayerReceiveMailEvent extends PlayerEvent implements Cancellable {
private Mail message;
public PlayerReceiveMailEvent(Player player, Mail message) {
super(player);
this.message = message;
}
public void setMessage(Mail message) {
this.message = message;
}
public Mail getMessage() {
return this.message;
}
}

View File

@ -0,0 +1,10 @@
package emu.grasscutter.server.event.types;
import emu.grasscutter.server.event.Event;
/**
* An event that is related to the game.
*/
public abstract class GameEvent extends Event {
}

View File

@ -0,0 +1,19 @@
package emu.grasscutter.server.event.types;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.event.Event;
/**
* An event that is related to player interactions.
*/
public abstract class PlayerEvent extends Event {
protected final Player player;
public PlayerEvent(Player player) {
this.player = player;
}
public Player getPlayer() {
return this.player;
}
}

View File

@ -1,4 +1,6 @@
package emu.grasscutter.server.event; package emu.grasscutter.server.event.types;
import emu.grasscutter.server.event.Event;
/** /**
* An event that is related to the internals of the server. * An event that is related to the internals of the server.

View File

@ -17,7 +17,7 @@ import emu.grasscutter.game.world.World;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
import emu.grasscutter.netty.KcpServer; import emu.grasscutter.netty.KcpServer;
import emu.grasscutter.server.event.ServerEvent; import emu.grasscutter.server.event.types.ServerEvent;
import emu.grasscutter.server.event.game.ServerTickEvent; import emu.grasscutter.server.event.game.ServerTickEvent;
import emu.grasscutter.server.event.internal.ServerStartEvent; import emu.grasscutter.server.event.internal.ServerStartEvent;
import emu.grasscutter.server.event.internal.ServerStopEvent; import emu.grasscutter.server.event.internal.ServerStopEvent;

View File

@ -13,6 +13,7 @@ import emu.grasscutter.net.packet.Opcodes;
import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.SetPlayerBornDataReqOuterClass.SetPlayerBornDataReq; import emu.grasscutter.net.proto.SetPlayerBornDataReqOuterClass.SetPlayerBornDataReq;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.server.event.game.PlayerCreationEvent;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.game.GameSession.SessionState; import emu.grasscutter.server.game.GameSession.SessionState;
@ -27,7 +28,7 @@ public class HandlerSetPlayerBornDataReq extends PacketHandler {
// Sanity checks // Sanity checks
int avatarId = req.getAvatarId(); int avatarId = req.getAvatarId();
int startingSkillDepot = 0; int startingSkillDepot;
if (avatarId == GameConstants.MAIN_CHARACTER_MALE) { if (avatarId == GameConstants.MAIN_CHARACTER_MALE) {
startingSkillDepot = 504; startingSkillDepot = 504;
} else if (avatarId == GameConstants.MAIN_CHARACTER_FEMALE) { } else if (avatarId == GameConstants.MAIN_CHARACTER_FEMALE) {
@ -41,8 +42,10 @@ public class HandlerSetPlayerBornDataReq extends PacketHandler {
nickname = "Traveler"; nickname = "Traveler";
} }
// Create character // Call creation event.
Player player = new Player(session); PlayerCreationEvent event = new PlayerCreationEvent(session, Player.class); event.call();
// Create player instance from event.
Player player = event.getPlayerClass().getDeclaredConstructor(GameSession.class).newInstance(session);
player.setNickname(nickname); player.setNickname(nickname);
try { try {
@ -92,5 +95,4 @@ public class HandlerSetPlayerBornDataReq extends PacketHandler {
session.close(); session.close();
} }
} }
} }

View File

@ -3,24 +3,12 @@ package emu.grasscutter.task;
import org.quartz.*; import org.quartz.*;
@PersistJobDataAfterExecution @PersistJobDataAfterExecution
public class TaskHandler implements Job { public abstract class TaskHandler implements Job {
public void restartExecute() throws JobExecutionException { public void restartExecute() throws JobExecutionException {
execute(null); execute(null);
} }
public void onEnable() { public abstract void onEnable();
}
public void onDisable() {
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// TODO Auto-generated method stub
}
public abstract void onDisable();
} }

View File

@ -4,22 +4,21 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.task.Task; import emu.grasscutter.task.Task;
import emu.grasscutter.task.TaskHandler; import emu.grasscutter.task.TaskHandler;
import org.quartz.JobExecutionContext; import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException; import org.quartz.JobExecutionException;
@Task(taskName = "MoonCard", taskCronExpression = "0 0 0 * * ?", triggerName = "MoonCardTrigger") @Task(taskName = "MoonCard", taskCronExpression = "0 0 0 * * ?", triggerName = "MoonCardTrigger")
// taskCronExpression: Fixed time period: 0:0:0 every day (twenty-four hour system) // taskCronExpression: Fixed time period: 0:0:0 every day (twenty-four hour system)
public class MoonCard extends TaskHandler { public final class MoonCard extends TaskHandler {
@Override @Override
public void onEnable() { public void onEnable() {
Grasscutter.getLogger().info("[Task] MoonCard task enabled."); Grasscutter.getLogger().debug("[Task] MoonCard task enabled.");
} }
@Override @Override
public void onDisable() { public void onDisable() {
Grasscutter.getLogger().info("[Task] MoonCard task disabled."); Grasscutter.getLogger().debug("[Task] MoonCard task disabled.");
} }
@Override @Override

View File

@ -0,0 +1,7 @@
package emu.grasscutter.utils;
import emu.grasscutter.server.event.Event;
public interface EventConsumer<T extends Event> {
void consume(T event);
}