diff --git a/api/src/main/java/com/velocitypowered/api/event/player/KickedFromServerEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/KickedFromServerEvent.java new file mode 100644 index 000000000..d92d44ec3 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/KickedFromServerEvent.java @@ -0,0 +1,98 @@ +package com.velocitypowered.api.event.player; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.event.ResultedEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.server.ServerInfo; +import net.kyori.text.Component; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Fired when a player is kicked from a server. You may either allow Velocity to kick the player (with an optional reason + * override) or redirect the player to a separate server. + */ +public class KickedFromServerEvent implements ResultedEvent { + private final Player player; + private final ServerInfo server; + private final Component originalReason; + private final boolean duringLogin; + private ServerKickResult result; + + public KickedFromServerEvent(Player player, ServerInfo server, Component originalReason, boolean duringLogin, Component fancyReason) { + this.player = Preconditions.checkNotNull(player, "player"); + this.server = Preconditions.checkNotNull(server, "server"); + this.originalReason = Preconditions.checkNotNull(originalReason, "originalReason"); + this.duringLogin = duringLogin; + this.result = new DisconnectPlayer(fancyReason); + } + + @Override + public ServerKickResult getResult() { + return result; + } + + @Override + public void setResult(@NonNull ServerKickResult result) { + this.result = Preconditions.checkNotNull(result, "result"); + } + + public Player getPlayer() { + return player; + } + + public ServerInfo getServer() { + return server; + } + + public Component getOriginalReason() { + return originalReason; + } + + public boolean kickedDuringLogin() { + return duringLogin; + } + + public interface ServerKickResult extends ResultedEvent.Result {} + + public static class DisconnectPlayer implements ServerKickResult { + private final Component component; + + private DisconnectPlayer(Component component) { + this.component = Preconditions.checkNotNull(component, "component"); + } + + @Override + public boolean isAllowed() { + return true; + } + + public Component getReason() { + return component; + } + + public static DisconnectPlayer create(Component component) { + return new DisconnectPlayer(component); + } + } + + public static class RedirectPlayer implements ServerKickResult { + private final ServerInfo server; + + private RedirectPlayer(ServerInfo server) { + this.server = Preconditions.checkNotNull(server, "server"); + } + + @Override + public boolean isAllowed() { + return false; + } + + public ServerInfo getServer() { + return server; + } + + public static RedirectPlayer create(ServerInfo info) { + return new RedirectPlayer(info); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index d8c6765f8..526578704 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -2,6 +2,7 @@ package com.velocitypowered.proxy.connection.client; import com.google.common.base.Preconditions; import com.google.gson.JsonObject; +import com.velocitypowered.api.event.player.KickedFromServerEvent; import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent; import com.velocitypowered.api.permission.PermissionFunction; @@ -36,6 +37,7 @@ import net.kyori.text.serializer.PlainComponentSerializer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import java.net.InetSocketAddress; import java.util.List; @@ -190,7 +192,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { logger.error("{}: unable to connect to server {}", this, info.getName(), throwable); userMessage = "Exception connecting to server " + info.getName(); } - handleConnectionException(info, TextComponent.builder() + handleConnectionException(info, null, TextComponent.builder() .content(userMessage + ": ") .color(TextColor.RED) .append(TextComponent.of(error, TextColor.WHITE)) @@ -205,14 +207,15 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } else { logger.error("{}: disconnected while connecting to {}: {}", this, info.getName(), plainTextReason); } - handleConnectionException(info, TextComponent.builder() + handleConnectionException(info, disconnectReason, TextComponent.builder() .content("Unable to connect to " + info.getName() + ": ") .color(TextColor.RED) .append(disconnectReason) .build()); } - public void handleConnectionException(ServerInfo info, Component disconnectReason) { + private void handleConnectionException(ServerInfo info, @Nullable Component kickReason, Component friendlyReason) { + boolean alreadyConnected = connectedServer != null && connectedServer.getServerInfo().equals(info);; connectionInFlight = null; if (connectedServer == null) { // The player isn't yet connected to a server. @@ -220,14 +223,27 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { if (nextServer.isPresent()) { createConnectionRequest(nextServer.get()).fireAndForget(); } else { - connection.closeWith(Disconnect.create(disconnectReason)); + connection.closeWith(Disconnect.create(friendlyReason)); } } else if (connectedServer.getServerInfo().equals(info)) { // Already connected to the server being disconnected from. - // TODO: ServerKickEvent - connection.closeWith(Disconnect.create(disconnectReason)); + if (kickReason != null) { + server.getEventManager().fire(new KickedFromServerEvent(this, info, kickReason, !alreadyConnected, friendlyReason)) + .thenAcceptAsync(event -> { + if (event.getResult() instanceof KickedFromServerEvent.DisconnectPlayer) { + KickedFromServerEvent.DisconnectPlayer res = (KickedFromServerEvent.DisconnectPlayer) event.getResult(); + connection.closeWith(Disconnect.create(res.getReason())); + } else if (event.getResult() instanceof KickedFromServerEvent.RedirectPlayer) { + KickedFromServerEvent.RedirectPlayer res = (KickedFromServerEvent.RedirectPlayer) event.getResult(); + createConnectionRequest(res.getServer()).fireAndForget(); + } else { + // In case someone gets creative, assume we want to disconnect the player. + connection.closeWith(Disconnect.create(friendlyReason)); + } + }, connection.getChannel().eventLoop()); + } } else { - connection.write(Chat.create(disconnectReason)); + connection.write(Chat.create(friendlyReason)); } }