Commit c442039f authored by Magix's avatar Magix Committed by GitHub
Browse files

Implement a plugin API for developers

Add a developer-friendly API to Grasscutter
parents d133e556 a82ec3e3
......@@ -37,7 +37,7 @@ sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
group = 'tech.xigam'
version = '1.0.0-dev'
version = '1.0.2-dev'
sourceCompatibility = 17
targetCompatibility = 17
......
......@@ -8,13 +8,14 @@ import java.util.List;
@Command(label = "coop", usage = "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
public void execute(Player sender, List<String> args) {
if (args.size() < 2) {
CommandHandler.sendMessage(sender, "Usage: coop <playerId> <target playerId>");
return;
}
try {
int tid = Integer.parseInt(args.get(0));
int hostId = Integer.parseInt(args.get(1));
......
......@@ -15,7 +15,7 @@ import java.util.*;
@Command(label = "giveall", usage = "giveall [player] [amount]",
description = "Gives all items", aliases = {"givea"}, permission = "player.giveall", threading = true)
public class GiveAllCommand implements CommandHandler {
public final class GiveAllCommand implements CommandHandler {
@Override
public void execute(Player sender, List<String> args) {
......@@ -142,16 +142,11 @@ public class GiveAllCommand implements CommandHandler {
}
}
if (testItemsList.contains(itemId)) {
return true;
}
return false;
return testItemsList.contains(itemId);
}
static class Range {
private int min;
private int max;
private final int min, max;
public Range(int min, int max) {
if(min > max){
......@@ -159,6 +154,7 @@ public class GiveAllCommand implements CommandHandler {
max ^= min;
min ^= max;
}
this.min = min;
this.max = max;
}
......
......@@ -6,24 +6,20 @@ import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.mail.Mail;
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.List;
import java.util.Locale;
@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")
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
// 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
// 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.
@Override
......@@ -48,7 +44,6 @@ public class SendMailCommand implements CommandHandler {
default -> {
if (DatabaseHelper.getPlayerById(Integer.parseInt(args.get(0))) != null) {
mailBuilder = new MailBuilder(Integer.parseInt(args.get(0)), new Mail());
break;
} else {
CommandHandler.sendMessage(sender, "The user with an id of '" + args.get(0) + "' does not exist");
return;
......@@ -73,7 +68,7 @@ public class SendMailCommand implements CommandHandler {
}
case "finish" -> {
if (mailBuilder.constructionStage == 3) {
if (mailBuilder.sendToAll == false) {
if (!mailBuilder.sendToAll) {
Grasscutter.getGameServer().getPlayerByUid(mailBuilder.recipient, true).sendMail(mailBuilder.mail);
CommandHandler.sendMessage(sender, "Message sent to user " + mailBuilder.recipient + "!");
} else {
......
......@@ -9,23 +9,25 @@ import java.util.List;
@Command(label = "tpall", usage = "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
public void execute(Player sender, List<String> args) {
if (sender == null) {
CommandHandler.sendMessage(null, "Run this command in-game.");
return;
}
if (!sender.getWorld().isMultiplayer()) {
CommandHandler.sendMessage(sender, "You only can use this command in MP mode.");
return;
}
for (Player gp : sender.getWorld().getPlayers()) {
if (gp.equals(sender))
for (Player player : sender.getWorld().getPlayers()) {
if (player.equals(sender))
continue;
Position pos = sender.getPos();
gp.getWorld().transferPlayerToScene(gp, sender.getSceneId(), pos);
player.getWorld().transferPlayerToScene(player, sender.getSceneId(), pos);
}
}
}
......@@ -37,6 +37,9 @@ import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass;
import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture;
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
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.GameSession;
import emu.grasscutter.server.packet.send.*;
......@@ -725,9 +728,13 @@ public class Player {
public List<Mail> getAllMail() { return this.mail; }
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.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()) {
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
......@@ -1037,6 +1044,11 @@ public class Player {
// First notify packets sent
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() {
......@@ -1055,6 +1067,9 @@ public class Player {
this.save();
this.getTeamManager().saveAvatars();
this.getFriendsList().save();
// Call quit event.
PlayerQuitEvent event = new PlayerQuitEvent(this); event.call();
}
public enum SceneLoadState {
......
package emu.grasscutter.plugin;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.plugin.api.ServerHook;
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.
*/
public abstract class Plugin {
private final ServerHook server = ServerHook.getInstance();
private PluginIdentifier identifier;
private URLClassLoader classLoader;
private File dataFolder;
/**
* This method is reflected into.
......@@ -15,10 +24,20 @@ public abstract class Plugin {
* Set plugin variables.
* @param identifier The plugin's identifier.
*/
private void initializePlugin(PluginIdentifier identifier) {
if(this.identifier == null)
private void initializePlugin(PluginIdentifier identifier, URLClassLoader classLoader) {
if(this.identifier != null) {
Grasscutter.getLogger().warn(this.identifier.name + " had a reinitialization attempt.");
return;
}
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.
*/
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. */
......
......@@ -3,9 +3,9 @@ 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.server.event.HandlerPriority;
import emu.grasscutter.utils.EventConsumer;
import emu.grasscutter.utils.Utils;
import org.reflections.Reflections;
import java.io.File;
import java.io.InputStreamReader;
......@@ -21,7 +21,7 @@ import java.util.jar.JarFile;
*/
public final class PluginManager {
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() {
this.loadPlugins(); // Load all plugins from the plugins directory.
......@@ -68,12 +68,12 @@ public final class PluginManager {
JarEntry entry = entries.nextElement();
if(entry.isDirectory() || !entry.getName().endsWith(".class") || entry.getName().contains("module-info")) continue;
String className = entry.getName().replace(".class", "").replace("/", ".");
Class<?> clazz = loader.loadClass(className);
loader.loadClass(className);
}
Class<?> pluginClass = loader.loadClass(pluginConfig.mainClass);
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.
} catch (ClassNotFoundException ignored) {
......@@ -89,14 +89,14 @@ public final class PluginManager {
* Load the specified plugin.
* @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);
// Add the plugin's identifier.
try {
Class<Plugin> pluginClass = Plugin.class;
Method method = pluginClass.getDeclaredMethod("initializePlugin", PluginIdentifier.class);
method.setAccessible(true); method.invoke(plugin, identifier); method.setAccessible(false);
Method method = pluginClass.getDeclaredMethod("initializePlugin", PluginIdentifier.class, URLClassLoader.class);
method.setAccessible(true); method.invoke(plugin, identifier, classLoader); method.setAccessible(false);
} catch (Exception ignored) {
Grasscutter.getLogger().warn("Failed to add plugin identifier: " + identifier.name);
}
......@@ -129,11 +129,10 @@ public final class PluginManager {
/**
* 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);
public void registerListener(EventHandler<? extends Event> listener) {
this.listeners.add(listener);
}
/**
......@@ -141,23 +140,31 @@ public final class PluginManager {
* @param event The event to invoke.
*/
public void invokeEvent(Event event) {
this.listeners.values().stream()
.flatMap(Collection::stream)
.forEach(listener -> this.invokeOnListener(listener, event));
EnumSet.allOf(HandlerPriority.class)
.forEach(priority -> this.checkAndFilter(event, priority));
}
/**
* 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) {
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) { }
private void checkAndFilter(Event event, HandlerPriority priority) {
this.listeners.stream()
.filter(handler -> handler.handles().isInstance(event))
.filter(handler -> handler.getPriority() == priority)
.toList().forEach(handler -> this.invokeHandler(event, handler));
}
/**
* 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);
}
}
\ No newline at end of file
package emu.grasscutter.plugin.api;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.dispatch.DispatchServer;
import emu.grasscutter.server.game.GameServer;
import java.util.LinkedList;
......@@ -11,7 +14,8 @@ import java.util.List;
*/
public final class ServerHook {
private static ServerHook instance;
private final GameServer server;
private final GameServer gameServer;
private final DispatchServer dispatchServer;
/**
* Gets the server hook instance.
......@@ -23,19 +27,47 @@ public final class ServerHook {
/**
* 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) {
this.server = server;
public ServerHook(GameServer gameServer, DispatchServer dispatchServer) {
this.gameServer = gameServer;
this.dispatchServer = dispatchServer;
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.
* @return Players connected to the server.
*/
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);
}
}
\ No newline at end of file
......@@ -30,6 +30,7 @@ import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.util.*;
......@@ -209,7 +210,7 @@ public final class DispatchServer {
return null;
}
private KeyManagerFactory createKeyManagerFactory(File keystore, String password) throws Exception {
private KeyManagerFactory createKeyManagerFactory(File keystore, String password) {
char[] pass = password.toCharArray();
KeyManagerFactory kmf = null;
......@@ -220,8 +221,8 @@ public final class DispatchServer {
kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, pass);
} catch (Exception e) {
throw e;
} catch (Exception exception) {
Grasscutter.getLogger().error("Unable to load keystore.", exception);
}
return kmf;
......@@ -243,10 +244,9 @@ public final class DispatchServer {
try {
kmf = createKeyManagerFactory(keystoreFile, "123456");
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) {
Grasscutter.getLogger().warn("[Dispatch] Error while loading keystore!");
e2.printStackTrace();
Grasscutter.getLogger().warn("[Dispatch] Error while loading keystore!", e2);
}
}
}
......@@ -257,7 +257,7 @@ public final class DispatchServer {
server = this.safelyCreateServer(this.getAddress());
}
HttpsServer httpsServer = null;
HttpsServer httpsServer;
try {
httpsServer = HttpsServer.create(getAddress(), 0);
......@@ -339,10 +339,6 @@ public final class DispatchServer {
// added.
account = DatabaseHelper.createAccountWithId(requestData.account, 0);
for (String permission : Grasscutter.getConfig().getDispatchOptions().defaultPermissions) {
account.addPermission(permission);
}
if (account != null) {
responseData.message = "OK";
responseData.data.account.uid = account.getId();
......@@ -352,6 +348,9 @@ public final class DispatchServer {
Grasscutter.getLogger()
.info(String.format("[Dispatch] Client %s failed to log in: Account %s created",
t.getRemoteAddress(), responseData.data.account.uid));
for (String permission : Grasscutter.getConfig().getDispatchOptions().defaultPermissions) {
account.addPermission(permission);
}
} else {
responseData.retcode = -201;
responseData.message = "Username not found, create failed.";
......@@ -575,15 +574,11 @@ public final class DispatchServer {
if (next > last) {
int eqPos = qs.indexOf('=', last);
try {
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 {
result.put(URLDecoder.decode(qs.substring(last, eqPos), "utf-8"),
URLDecoder.decode(qs.substring(eqPos + 1, next), "utf-8"));
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // will never happen, utf-8 support is mandatory for java
result.put(URLDecoder.decode(qs.substring(last, eqPos), StandardCharsets.UTF_8),
URLDecoder.decode(qs.substring(eqPos + 1, next), StandardCharsets.UTF_8));
}
}
last = next + 1;
......
package emu.grasscutter.server.event;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.utils.EventConsumer;
/**
* Declares a class as an event listener/handler.
public final class EventHandler<T extends Event> {
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.
*/
public Class<T> handles() {
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.
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface EventHandler {
public void register() {
Grasscutter.getPluginManager().registerListener(this);
}
}
\ No newline at end of file
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
}
package emu.grasscutter.server.event;
/**
* Implementing this interface declares a class as an event listener.
*/
public interface Listener {
}
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 {
private String regionList;
......
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 {
private String regionInfo;
......
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;
}
}
package emu.grasscutter.server.event.game;
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;
public final class ReceivePacketEvent extends ServerEvent implements Cancellable {
......
......@@ -2,7 +2,7 @@ package emu.grasscutter.server.event.game;
import emu.grasscutter.net.packet.BasePacket;
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;
public final class SendPacketEvent extends ServerEvent implements Cancellable {
......
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 ServerTickEvent() {
......
package emu.grasscutter.server.event.internal;
import emu.grasscutter.server.event.ServerEvent;
import emu.grasscutter.server.event.types.ServerEvent;
import java.time.OffsetDateTime;
......
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