From d49dc3f24bb8aa8798766b3808d7269cfe040cdf Mon Sep 17 00:00:00 2001
From: Jake Potrebic <jake.m.potrebic@gmail.com>
Date: Tue, 25 May 2021 11:20:28 -0700
Subject: [PATCH] Add PlayerKickEvent causes (#5648)

---
 .../Add-PlayerKickEvent-causes.patch          | 127 ++++++
 .../Add-PlayerKickEvent-causes.patch          | 393 ++++++++++++++++++
 2 files changed, 520 insertions(+)
 create mode 100644 Spigot-API-Patches/Add-PlayerKickEvent-causes.patch
 create mode 100644 Spigot-Server-Patches/Add-PlayerKickEvent-causes.patch

diff --git a/Spigot-API-Patches/Add-PlayerKickEvent-causes.patch b/Spigot-API-Patches/Add-PlayerKickEvent-causes.patch
new file mode 100644
index 0000000000..11a7219daf
--- /dev/null
+++ b/Spigot-API-Patches/Add-PlayerKickEvent-causes.patch
@@ -0,0 +1,127 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <jake.m.potrebic@gmail.com>
+Date: Sat, 15 May 2021 20:30:34 -0700
+Subject: [PATCH] Add PlayerKickEvent causes
+
+
+diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/entity/Player.java
++++ b/src/main/java/org/bukkit/entity/Player.java
+@@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+      * @param message kick message
+      */
+     void kick(final @Nullable net.kyori.adventure.text.Component message);
++
++    /**
++     * Kicks player with custom kick message and cause.
++     *
++     * @param message kick message
++     * @param cause kick cause
++     */
++    void kick(final @Nullable Component message, @NotNull org.bukkit.event.player.PlayerKickEvent.Cause cause);
+     // Paper end
+ 
+     /**
+diff --git a/src/main/java/org/bukkit/event/player/PlayerKickEvent.java b/src/main/java/org/bukkit/event/player/PlayerKickEvent.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/event/player/PlayerKickEvent.java
++++ b/src/main/java/org/bukkit/event/player/PlayerKickEvent.java
+@@ -0,0 +0,0 @@ public class PlayerKickEvent extends PlayerEvent implements Cancellable {
+     private static final HandlerList handlers = new HandlerList();
+     private net.kyori.adventure.text.Component leaveMessage; // Paper
+     private net.kyori.adventure.text.Component kickReason; // Paper
++    private final Cause cause; // Paper
+     private Boolean cancel;
+ 
+     @Deprecated // Paper
+@@ -0,0 +0,0 @@ public class PlayerKickEvent extends PlayerEvent implements Cancellable {
+         super(playerKicked);
+         this.kickReason = org.bukkit.Bukkit.getUnsafe().legacyComponentSerializer().deserialize(kickReason); // Paper
+         this.leaveMessage = org.bukkit.Bukkit.getUnsafe().legacyComponentSerializer().deserialize(leaveMessage); // Paper
++        this.cause  = Cause.UNKNOWN; // Paper
+         this.cancel = false;
+     }
+     // Paper start
++    @Deprecated
+     public PlayerKickEvent(@NotNull final Player playerKicked, @NotNull final net.kyori.adventure.text.Component kickReason, @NotNull final net.kyori.adventure.text.Component leaveMessage) {
+         super(playerKicked);
+         this.kickReason = kickReason;
+         this.leaveMessage = leaveMessage;
+         this.cancel = false;
++        this.cause = Cause.UNKNOWN;
++    }
++
++    public PlayerKickEvent(@NotNull final Player playerKicked, @NotNull final net.kyori.adventure.text.Component kickReason, @NotNull final net.kyori.adventure.text.Component leaveMessage, @NotNull final Cause cause) {
++        super(playerKicked);
++        this.kickReason = kickReason;
++        this.leaveMessage = leaveMessage;
++        this.cancel = false;
++        this.cause = cause;
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ public class PlayerKickEvent extends PlayerEvent implements Cancellable {
+     public static HandlerList getHandlerList() {
+         return handlers;
+     }
++    // Paper start
++    /**
++     * Gets the cause of this kick
++     *
++     * @return
++     */
++    @NotNull
++    public org.bukkit.event.player.PlayerKickEvent.Cause getCause() {
++        return cause;
++    }
++
++    public enum Cause {
++
++        PLUGIN,
++
++        WHITELIST,
++
++        BANNED,
++
++        IP_BANNED,
++
++        KICK_COMMAND,
++
++        FLYING_PLAYER,
++
++        FLYING_VEHICLE,
++
++        TIMEOUT,
++
++        IDLING,
++
++        INVALID_VEHICLE_MOVEMENT,
++
++        INVALID_PLAYER_MOVEMENT,
++
++        INVALID_ENTITY_ATTACKED,
++
++        INVALID_PAYLOAD,
++
++        SPAM,
++
++        ILLEGAL_ACTION,
++
++        ILLEGAL_CHARACTERS,
++
++        SELF_INTERACTION,
++
++        DUPLICATE_LOGIN,
++
++        /**
++         * Spigot's restart command
++         */
++        RESTART_COMMAND,
++        /**
++         * Fallback cause
++         */
++        UNKNOWN,
++    }
++    // Paper end
+ }
diff --git a/Spigot-Server-Patches/Add-PlayerKickEvent-causes.patch b/Spigot-Server-Patches/Add-PlayerKickEvent-causes.patch
new file mode 100644
index 0000000000..4fe4eaf2c7
--- /dev/null
+++ b/Spigot-Server-Patches/Add-PlayerKickEvent-causes.patch
@@ -0,0 +1,393 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <jake.m.potrebic@gmail.com>
+Date: Sat, 15 May 2021 20:30:45 -0700
+Subject: [PATCH] Add PlayerKickEvent causes
+
+
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
+                 EntityPlayer entityplayer = (EntityPlayer) iterator.next();
+ 
+                 if (!whitelist.isWhitelisted(entityplayer.getProfile())) {
+-                    entityplayer.playerConnection.disconnect(org.spigotmc.SpigotConfig.whitelistMessage); // Paper - use configurable message
++                    entityplayer.playerConnection.disconnect(org.spigotmc.SpigotConfig.whitelistMessage, org.bukkit.event.player.PlayerKickEvent.Cause.WHITELIST); // Paper - use configurable message
+                 }
+             }
+ 
+diff --git a/src/main/java/net/minecraft/server/commands/CommandBan.java b/src/main/java/net/minecraft/server/commands/CommandBan.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/commands/CommandBan.java
++++ b/src/main/java/net/minecraft/server/commands/CommandBan.java
+@@ -0,0 +0,0 @@ public class CommandBan {
+                 EntityPlayer entityplayer = commandlistenerwrapper.getServer().getPlayerList().getPlayer(gameprofile.getId());
+ 
+                 if (entityplayer != null) {
+-                    entityplayer.playerConnection.disconnect(new ChatMessage("multiplayer.disconnect.banned"));
++                    entityplayer.playerConnection.disconnect(new ChatMessage("multiplayer.disconnect.banned"), org.bukkit.event.player.PlayerKickEvent.Cause.BANNED); // Paper - kick event cause
+                 }
+             }
+         }
+diff --git a/src/main/java/net/minecraft/server/commands/CommandBanIp.java b/src/main/java/net/minecraft/server/commands/CommandBanIp.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/commands/CommandBanIp.java
++++ b/src/main/java/net/minecraft/server/commands/CommandBanIp.java
+@@ -0,0 +0,0 @@ public class CommandBanIp {
+             while (iterator.hasNext()) {
+                 EntityPlayer entityplayer = (EntityPlayer) iterator.next();
+ 
+-                entityplayer.playerConnection.disconnect(new ChatMessage("multiplayer.disconnect.ip_banned"));
++                entityplayer.playerConnection.disconnect(new ChatMessage("multiplayer.disconnect.ip_banned"), org.bukkit.event.player.PlayerKickEvent.Cause.IP_BANNED); // Paper - kick event cause
+             }
+ 
+             return list.size();
+diff --git a/src/main/java/net/minecraft/server/commands/CommandKick.java b/src/main/java/net/minecraft/server/commands/CommandKick.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/commands/CommandKick.java
++++ b/src/main/java/net/minecraft/server/commands/CommandKick.java
+@@ -0,0 +0,0 @@ public class CommandKick {
+         while (iterator.hasNext()) {
+             EntityPlayer entityplayer = (EntityPlayer) iterator.next();
+ 
+-            entityplayer.playerConnection.disconnect(ichatbasecomponent);
++            entityplayer.playerConnection.disconnect(ichatbasecomponent, org.bukkit.event.player.PlayerKickEvent.Cause.KICK_COMMAND); // Paper - kick event cause
+             commandlistenerwrapper.sendMessage(new ChatMessage("commands.kick.success", new Object[]{entityplayer.getScoreboardDisplayName(), ichatbasecomponent}), true);
+         }
+ 
+diff --git a/src/main/java/net/minecraft/server/network/PlayerConnection.java b/src/main/java/net/minecraft/server/network/PlayerConnection.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/network/PlayerConnection.java
++++ b/src/main/java/net/minecraft/server/network/PlayerConnection.java
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+         if (this.B && !this.player.isSleeping()) {
+             if (++this.C > 80) {
+                 PlayerConnection.LOGGER.warn("{} was kicked for floating too long!", this.player.getDisplayName().getString());
+-                this.disconnect(com.destroystokyo.paper.PaperConfig.flyingKickPlayerMessage); // Paper - use configurable kick message
++                this.disconnect(com.destroystokyo.paper.PaperConfig.flyingKickPlayerMessage, org.bukkit.event.player.PlayerKickEvent.Cause.FLYING_PLAYER); // Paper - use configurable kick message & kick event cause
+                 return;
+             }
+         } else {
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+             if (this.D && this.player.getRootVehicle().getRidingPassenger() == this.player) {
+                 if (++this.E > 80) {
+                     PlayerConnection.LOGGER.warn("{} was kicked for floating a vehicle too long!", this.player.getDisplayName().getString());
+-                    this.disconnect(com.destroystokyo.paper.PaperConfig.flyingKickVehicleMessage); // Paper - use configurable kick message
++                    this.disconnect(com.destroystokyo.paper.PaperConfig.flyingKickVehicleMessage, org.bukkit.event.player.PlayerKickEvent.Cause.FLYING_VEHICLE); // Paper - use configurable kick message & kick event cause
+                     return;
+                 }
+             } else {
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+         if (this.isPendingPing()) {
+             if (!this.processedDisconnect && elapsedTime >= KEEPALIVE_LIMIT) { // check keepalive limit, don't fire if already disconnected
+                 PlayerConnection.LOGGER.warn("{} was kicked due to keepalive timeout!", this.player.getName()); // more info
+-                this.disconnect(new ChatMessage("disconnect.timeout", new Object[0]));
++                this.disconnect(new ChatMessage("disconnect.timeout", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause
+             }
+         } else {
+             if (elapsedTime >= 15000L) { // 15 seconds
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+ 
+         if (this.player.F() > 0L && this.minecraftServer.getIdleTimeout() > 0 && SystemUtils.getMonotonicMillis() - this.player.F() > (long) (this.minecraftServer.getIdleTimeout() * 1000 * 60)) {
+             this.player.resetIdleTimer(); // CraftBukkit - SPIGOT-854
+-            this.disconnect(new ChatMessage("multiplayer.disconnect.idling"));
++            this.disconnect(new ChatMessage("multiplayer.disconnect.idling"), org.bukkit.event.player.PlayerKickEvent.Cause.IDLING); // Paper - kick event cause
+         }
+ 
+     }
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+ 
+     public void disconnect(String s) {
+         // Paper start
+-        this.disconnect(PaperAdventure.LEGACY_SECTION_UXRC.deserialize(s));
++        this.disconnect(PaperAdventure.LEGACY_SECTION_UXRC.deserialize(s), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
++    }
++
++    public void disconnect(String s, PlayerKickEvent.Cause cause) {
++        this.disconnect(PaperAdventure.LEGACY_SECTION_UXRC.deserialize(s), cause);
+     }
+ 
+     public void disconnect(final IChatBaseComponent reason) {
+-        this.disconnect(PaperAdventure.asAdventure(reason));
++        this.disconnect(PaperAdventure.asAdventure(reason), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
++    }
++
++    public void disconnect(final IChatBaseComponent reason, PlayerKickEvent.Cause cause) {
++        this.disconnect(PaperAdventure.asAdventure(reason), cause);
+     }
+ 
+-    public void disconnect(net.kyori.adventure.text.Component reason) {
++    public void disconnect(net.kyori.adventure.text.Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
+         // Paper end
+         // CraftBukkit start - fire PlayerKickEvent
+         if (this.processedDisconnect) {
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+         }
+         net.kyori.adventure.text.Component leaveMessage = net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, this.player.getBukkitEntity().displayName()); // Paper - Adventure
+ 
+-        PlayerKickEvent event = new PlayerKickEvent(this.server.getPlayer(this.player), reason, leaveMessage); // Paper - Adventure
++        PlayerKickEvent event = new PlayerKickEvent(this.server.getPlayer(this.player), reason, leaveMessage, cause); // Paper - Adventure & kick event reason
+ 
+         if (this.server.getServer().isRunning()) {
+             this.server.getPluginManager().callEvent(event);
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+     public void a(PacketPlayInVehicleMove packetplayinvehiclemove) {
+         PlayerConnectionUtils.ensureMainThread(packetplayinvehiclemove, this, this.player.getWorldServer());
+         if (b(packetplayinvehiclemove)) {
+-            this.disconnect(new ChatMessage("multiplayer.disconnect.invalid_vehicle_movement"));
++            this.disconnect(new ChatMessage("multiplayer.disconnect.invalid_vehicle_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_VEHICLE_MOVEMENT); // Paper - kick event cause
+         } else {
+             Entity entity = this.player.getRootVehicle();
+ 
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+         // PlayerConnectionUtils.ensureMainThread(packetplayintabcomplete, this, this.player.getWorldServer()); // Paper - run this async
+         // CraftBukkit start
+         if (tabSpamLimiter.addAndGet(com.destroystokyo.paper.PaperConfig.tabSpamIncrement) > com.destroystokyo.paper.PaperConfig.tabSpamLimit && !this.minecraftServer.getPlayerList().isOp(this.player.getProfile())) { // Paper start - split and make configurable
+-            minecraftServer.scheduleOnMain(() -> this.disconnect(new ChatMessage("disconnect.spam", new Object[0]))); // Paper
++            minecraftServer.scheduleOnMain(() -> this.disconnect(new ChatMessage("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause
+             return;
+         }
+         // Paper start
+         String str = packetplayintabcomplete.c(); int index = -1;
+         if (str.length() > 64 && ((index = str.indexOf(' ')) == -1 || index >= 64)) {
+-            minecraftServer.scheduleOnMain(() -> this.disconnect(new ChatMessage("disconnect.spam", new Object[0]))); // Paper
++            minecraftServer.scheduleOnMain(() -> this.disconnect(new ChatMessage("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause
+             return;
+         }
+         // Paper end
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+         // Paper start - validate pick item position
+         if (!(packetplayinpickitem.b() >= 0 && packetplayinpickitem.b() < this.player.inventory.items.size())) {
+             PlayerConnection.LOGGER.warn("{} tried to set an invalid carried item", this.player.getDisplayName().getString());
+-            this.disconnect("Invalid hotbar selection (Hacking?)");
++            this.disconnect("Invalid hotbar selection (Hacking?)", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause
+             return;
+         }
+         this.player.inventory.c(packetplayinpickitem.b()); // Paper - Diff above if changed
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+             NBTTagList pageList = testStack.getTag().getList("pages", 8);
+             if (pageList.size() > 100) {
+                 PlayerConnection.LOGGER.warn(this.player.getName() + " tried to send a book with too many pages");
+-                minecraftServer.scheduleOnMain(() -> this.disconnect("Book too large!"));
++                minecraftServer.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause
+                 return;
+             }
+             long byteTotal = 0;
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+                 int byteLength = testString.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
+                 if (byteLength > 256 * 4) {
+                     PlayerConnection.LOGGER.warn(this.player.getName() + " tried to send a book with with a page too large!");
+-                    minecraftServer.scheduleOnMain(() -> this.disconnect("Book too large!"));
++                    minecraftServer.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause
+                     return;
+                 }
+                 byteTotal += byteLength;
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+ 
+             if (byteTotal > byteAllowed) {
+                 PlayerConnection.LOGGER.warn(this.player.getName() + " tried to send too large of a book. Book Size: " + byteTotal + " - Allowed:  "+ byteAllowed + " - Pages: " + pageList.size());
+-                minecraftServer.scheduleOnMain(() -> this.disconnect("Book too large!"));
++                minecraftServer.scheduleOnMain(() -> this.disconnect("Book too large!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION)); // Paper - kick event cause
+                 return;
+             }
+         }
+         // Paper end
+         // CraftBukkit start
+         if (this.lastBookTick + 20 > MinecraftServer.currentTick) {
+-            this.disconnect("Book edited too quickly!");
++            this.disconnect("Book edited too quickly!", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause
+             return;
+         }
+         this.lastBookTick = MinecraftServer.currentTick;
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+     public void a(PacketPlayInFlying packetplayinflying) {
+         PlayerConnectionUtils.ensureMainThread(packetplayinflying, this, this.player.getWorldServer());
+         if (b(packetplayinflying)) {
+-            this.disconnect(new ChatMessage("multiplayer.disconnect.invalid_player_movement"));
++            this.disconnect(new ChatMessage("multiplayer.disconnect.invalid_player_movement"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PLAYER_MOVEMENT); // Paper - kick event cause
+         } else {
+             WorldServer worldserver = this.player.getWorldServer();
+ 
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+                         this.dropCount++;
+                         if (this.dropCount >= 20) {
+                             LOGGER.warn(this.player.getName() + " dropped their items too quickly!");
+-                            this.disconnect("You dropped your items too quickly (Hacking?)");
++                            this.disconnect("You dropped your items too quickly (Hacking?)", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause
+                             return;
+                         }
+                     }
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+             this.player.resetIdleTimer();
+         } else {
+             PlayerConnection.LOGGER.warn("{} tried to set an invalid carried item", this.player.getDisplayName().getString());
+-            this.disconnect("Invalid hotbar selection (Hacking?)"); // CraftBukkit
++            this.disconnect("Invalid hotbar selection (Hacking?)", org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // CraftBukkit // Paper - kick event cause
+         }
+     }
+ 
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+                         Waitable waitable = new Waitable() {
+                             @Override
+                             protected Object evaluate() {
+-                                PlayerConnection.this.disconnect(new ChatMessage("multiplayer.disconnect.illegal_characters"));
++                                PlayerConnection.this.disconnect(new ChatMessage("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - kick event cause
+                                 return null;
+                             }
+                         };
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+                             throw new RuntimeException(e);
+                         }
+                     } else {
+-                        this.disconnect(new ChatMessage("multiplayer.disconnect.illegal_characters"));
++                        this.disconnect(new ChatMessage("multiplayer.disconnect.illegal_characters"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_CHARACTERS); // Paper - kick event cause
+                     }
+                     // CraftBukkit end
+                     return;
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+                     Waitable waitable = new Waitable() {
+                         @Override
+                         protected Object evaluate() {
+-                            PlayerConnection.this.disconnect(new ChatMessage("disconnect.spam"));
++                            PlayerConnection.this.disconnect(new ChatMessage("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause
+                             return null;
+                         }
+                     };
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+                         throw new RuntimeException(e);
+                     }
+                 } else {
+-                    this.disconnect(new ChatMessage("disconnect.spam"));
++                    this.disconnect(new ChatMessage("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause
+                 }
+                 // CraftBukkit end
+             }
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+         // Spigot Start
+         if ( entity == player && !player.isSpectator() )
+         {
+-            disconnect( "Cannot interact with self!" );
++            disconnect( "Cannot interact with self!", org.bukkit.event.player.PlayerKickEvent.Cause.SELF_INTERACTION ); // Paper - kick event cause
+             return;
+         }
+         // Spigot End
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+                     // CraftBukkit end
+                 } else if (packetplayinuseentity.b() == PacketPlayInUseEntity.EnumEntityUseAction.ATTACK) {
+                     if (entity instanceof EntityItem || entity instanceof EntityExperienceOrb || entity instanceof EntityArrow || (entity == this.player && !player.isSpectator())) { // CraftBukkit
+-                        this.disconnect(new ChatMessage("multiplayer.disconnect.invalid_entity_attacked"));
++                        this.disconnect(new ChatMessage("multiplayer.disconnect.invalid_entity_attacked"), org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_ENTITY_ATTACKED); // Paper - kick event cause
+                         PlayerConnection.LOGGER.warn("Player {} tried to attack an invalid entity", this.player.getDisplayName().getString());
+                         return;
+                     }
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+         // Paper start
+         if (!Bukkit.isPrimaryThread()) {
+             if (recipeSpamPackets.addAndGet(PaperConfig.autoRecipeIncrement) > PaperConfig.autoRecipeLimit) {
+-                minecraftServer.scheduleOnMain(() -> this.disconnect(new ChatMessage("disconnect.spam", new Object[0]))); // Paper
++                minecraftServer.scheduleOnMain(() -> this.disconnect(new ChatMessage("disconnect.spam", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM)); // Paper - kick event cause
+                 return;
+             }
+         }
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+         } else if (!this.isExemptPlayer()) {
+             // Paper start - This needs to be handled on the main thread for plugins
+             minecraftServer.scheduleOnMain(() -> {
+-            this.disconnect(new ChatMessage("disconnect.timeout"));
++            this.disconnect(new ChatMessage("disconnect.timeout"), org.bukkit.event.player.PlayerKickEvent.Cause.TIMEOUT); // Paper - kick event cause
+             });
+             // Paper end
+         }
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+                 }
+             } catch (Exception ex) {
+                 PlayerConnection.LOGGER.error("Couldn\'t register custom payload", ex);
+-                this.disconnect("Invalid payload REGISTER!");
++                this.disconnect("Invalid payload REGISTER!", org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
+             }
+         } else if (packetplayincustompayload.tag.equals(CUSTOM_UNREGISTER)) {
+             try {
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+                 }
+             } catch (Exception ex) {
+                 PlayerConnection.LOGGER.error("Couldn\'t unregister custom payload", ex);
+-                this.disconnect("Invalid payload UNREGISTER!");
++                this.disconnect("Invalid payload UNREGISTER!", org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
+             }
+         } else {
+             try {
+@@ -0,0 +0,0 @@ public class PlayerConnection implements PacketListenerPlayIn {
+                 server.getMessenger().dispatchIncomingMessage(player.getBukkitEntity(), packetplayincustompayload.tag.toString(), data);
+             } catch (Exception ex) {
+                 PlayerConnection.LOGGER.error("Couldn\'t dispatch custom payload", ex);
+-                this.disconnect("Invalid custom payload!");
++                this.disconnect("Invalid custom payload!", org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); // Paper - kick event cause
+             }
+         }
+ 
+diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/players/PlayerList.java
++++ b/src/main/java/net/minecraft/server/players/PlayerList.java
+@@ -0,0 +0,0 @@ public abstract class PlayerList {
+         while (iterator.hasNext()) {
+             entityplayer = (EntityPlayer) iterator.next();
+             savePlayerFile(entityplayer); // CraftBukkit - Force the player's inventory to be saved
+-            entityplayer.playerConnection.disconnect(new ChatMessage("multiplayer.disconnect.duplicate_login", new Object[0]));
++            entityplayer.playerConnection.disconnect(new ChatMessage("multiplayer.disconnect.duplicate_login", new Object[0]), org.bukkit.event.player.PlayerKickEvent.Cause.DUPLICATE_LOGIN); // Paper - kick event cause
+         }
+ 
+         // Instead of kicking then returning, we need to store the kick reason
+@@ -0,0 +0,0 @@ public abstract class PlayerList {
+     public void shutdown(boolean isRestarting) {
+         // CraftBukkit start - disconnect safely
+         for (EntityPlayer player : this.players) {
+-            if (isRestarting) player.playerConnection.disconnect(org.spigotmc.SpigotConfig.restartMessage); else // Paper
+-            player.playerConnection.disconnect(this.server.server.shutdownMessage()); // CraftBukkit - add custom shutdown message // Paper - Adventure
++            if (isRestarting) player.playerConnection.disconnect(org.spigotmc.SpigotConfig.restartMessage, org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); else // Paper - kick event cause (cause is never used here)
++            player.playerConnection.disconnect(this.server.server.shutdownMessage(), org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); // CraftBukkit - add custom shutdown message // Paper - Adventure & KickEventCause (cause is never used here)
+         }
+         // CraftBukkit end
+ 
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+         org.spigotmc.AsyncCatcher.catchOp("player kick"); // Spigot
+         if (getHandle().playerConnection == null) return;
+ 
+-        getHandle().playerConnection.disconnect(message == null ? "" : message);
++        getHandle().playerConnection.disconnect(message == null ? "" : message, org.bukkit.event.player.PlayerKickEvent.Cause.PLUGIN); // Paper - kick event cause
+     }
+ 
+     // Paper start
+     @Override
+     public void kick(final net.kyori.adventure.text.Component message) {
++        kick(message, org.bukkit.event.player.PlayerKickEvent.Cause.PLUGIN);
++    }
++
++    @Override
++    public void kick(net.kyori.adventure.text.Component message, org.bukkit.event.player.PlayerKickEvent.Cause cause) {
+         org.spigotmc.AsyncCatcher.catchOp("player kick");
+         final PlayerConnection connection = this.getHandle().playerConnection;
+         if (connection != null) {
+-            connection.disconnect(message == null ? net.kyori.adventure.text.Component.empty() : message);
++            connection.disconnect(message == null ? net.kyori.adventure.text.Component.empty() : message, cause);
+         }
+     }
+     // Paper end
+diff --git a/src/main/java/org/spigotmc/RestartCommand.java b/src/main/java/org/spigotmc/RestartCommand.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/spigotmc/RestartCommand.java
++++ b/src/main/java/org/spigotmc/RestartCommand.java
+@@ -0,0 +0,0 @@ public class RestartCommand extends Command
+             // Kick all players
+             for ( EntityPlayer p : com.google.common.collect.ImmutableList.copyOf( MinecraftServer.getServer().getPlayerList().players ) )
+             {
+-                p.playerConnection.disconnect(SpigotConfig.restartMessage);
++                p.playerConnection.disconnect(SpigotConfig.restartMessage, org.bukkit.event.player.PlayerKickEvent.Cause.RESTART_COMMAND); // Paper - kick event reason (cause is never used))
+             }
+             // Give the socket a chance to send the packets
+             try