From 17f3b919ea8f6e3c66579a3df1532637e0b4d8ee Mon Sep 17 00:00:00 2001
From: Jake Potrebic <jake.m.potrebic@gmail.com>
Date: Tue, 18 May 2021 01:35:39 -0700
Subject: [PATCH] Add cause and cancel message to PlayerGameModeChangeEvent
 (#5638)

---
 ...ditions-to-PlayerGameModeChangeEvent.patch | 111 +++++++++++++++
 ...ditions-to-PlayerGameModeChangeEvent.patch | 128 ++++++++++++++++++
 2 files changed, 239 insertions(+)
 create mode 100644 Spigot-API-Patches/additions-to-PlayerGameModeChangeEvent.patch
 create mode 100644 Spigot-Server-Patches/additions-to-PlayerGameModeChangeEvent.patch

diff --git a/Spigot-API-Patches/additions-to-PlayerGameModeChangeEvent.patch b/Spigot-API-Patches/additions-to-PlayerGameModeChangeEvent.patch
new file mode 100644
index 0000000000..baac623927
--- /dev/null
+++ b/Spigot-API-Patches/additions-to-PlayerGameModeChangeEvent.patch
@@ -0,0 +1,111 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <jake.m.potrebic@gmail.com>
+Date: Sat, 15 May 2021 10:04:50 -0700
+Subject: [PATCH] additions to PlayerGameModeChangeEvent
+
+
+diff --git a/src/main/java/org/bukkit/event/player/PlayerGameModeChangeEvent.java b/src/main/java/org/bukkit/event/player/PlayerGameModeChangeEvent.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/org/bukkit/event/player/PlayerGameModeChangeEvent.java
++++ b/src/main/java/org/bukkit/event/player/PlayerGameModeChangeEvent.java
+@@ -0,0 +0,0 @@ public class PlayerGameModeChangeEvent extends PlayerEvent implements Cancellabl
+     private static final HandlerList handlers = new HandlerList();
+     private boolean cancelled;
+     private final GameMode newGameMode;
++    // Paper start
++    private final Cause cause;
++    private net.kyori.adventure.text.Component cancelMessage;
+ 
++    @Deprecated // Paper end
+     public PlayerGameModeChangeEvent(@NotNull final Player player, @NotNull final GameMode newGameMode) {
++        // Paper start
++        this(player, newGameMode, Cause.UNKNOWN, null);
++    }
++
++    public PlayerGameModeChangeEvent(@NotNull final Player player, @NotNull final GameMode newGameMode, @NotNull Cause cause, @org.jetbrains.annotations.Nullable net.kyori.adventure.text.Component cancelMessage) {
++        // Paper end
+         super(player);
+         this.newGameMode = newGameMode;
++        this.cause = cause; // Paper
++        this.cancelMessage = cancelMessage; // Paper
+     }
+ 
+     @Override
+@@ -0,0 +0,0 @@ public class PlayerGameModeChangeEvent extends PlayerEvent implements Cancellabl
+     public static HandlerList getHandlerList() {
+         return handlers;
+     }
++    // Paper start
++    /**
++     * Gets the cause of this gamemode change.
++     *
++     * @return the cause
++     */
++    @NotNull
++    public Cause getCause() {
++        return cause;
++    }
++
++    /**
++     * <b>Only valid if the cause of the gamemode change was directly due to a command.</b>.
++     * Gets the message shown to the command user if the event is cancelled
++     * as a notification that a player's gamemode was not changed.
++     * <p>
++     * This returns {@code null} if the gamemode change was due to a plugin, or a
++     * player joining the game with a gamemode not equal to the server default gamemode
++     * and {@code force-gamemode} is set to true.
++     *
++     * @return the error message shown to the command user, null if not directly caused by a command
++     */
++    @org.jetbrains.annotations.Nullable
++    public net.kyori.adventure.text.Component cancelMessage() {
++        return cancelMessage;
++    }
++
++    /**
++     * Sets the message shown to the command user if the event was cancelled.
++     * <b>The message is only shown to cancelled events that are directly called by a command
++     * not by a plugin or a player joining with the wrong gamemode.</b>
++     *
++     * @param message the error message shown to the command user, null to show no message.
++     */
++    public void cancelMessage(@org.jetbrains.annotations.Nullable net.kyori.adventure.text.Component message) {
++        this.cancelMessage = message;
++    }
++
++    public enum Cause {
++
++        /**
++         * A plugin changed the player's gamemode with
++         * {@link Player#setGameMode(GameMode)}.
++         */
++        PLUGIN,
++
++        /**
++         * The {@code /gamemode} command was used.
++         */
++        COMMAND,
++
++        /**
++         * A player had their gamemode changed as a result of
++         * the {@code /defaultgamemode} command, or they joined
++         * with a gamemode that was not the default gamemode and
++         * {@code force-gamemode} in {@code server.properties} is set to true.
++         */
++        DEFAULT_GAMEMODE,
++
++        /**
++         * When the player dies in a hardcore world and has their gamemode
++         * changed to {@link GameMode#SPECTATOR}.
++         */
++        HARDCORE_DEATH,
++
++        /**
++         * This cause is only used if a plugin fired their own
++         * {@link PlayerGameModeChangeEvent} and did not include a
++         * cause. Can usually be ignored.
++         */
++        UNKNOWN,
++    }
++    // Paper end
+ }
diff --git a/Spigot-Server-Patches/additions-to-PlayerGameModeChangeEvent.patch b/Spigot-Server-Patches/additions-to-PlayerGameModeChangeEvent.patch
new file mode 100644
index 0000000000..97d40eb493
--- /dev/null
+++ b/Spigot-Server-Patches/additions-to-PlayerGameModeChangeEvent.patch
@@ -0,0 +1,128 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <jake.m.potrebic@gmail.com>
+Date: Sat, 15 May 2021 10:04:43 -0700
+Subject: [PATCH] additions to PlayerGameModeChangeEvent
+
+
+diff --git a/src/main/java/net/minecraft/server/commands/CommandGamemode.java b/src/main/java/net/minecraft/server/commands/CommandGamemode.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/commands/CommandGamemode.java
++++ b/src/main/java/net/minecraft/server/commands/CommandGamemode.java
+@@ -0,0 +0,0 @@ public class CommandGamemode {
+             EntityPlayer entityplayer = (EntityPlayer) iterator.next();
+ 
+             if (entityplayer.playerInteractManager.getGameMode() != enumgamemode) {
+-                entityplayer.a(enumgamemode);
+-                // CraftBukkit start - handle event cancelling the change
+-                if (entityplayer.playerInteractManager.getGameMode() != enumgamemode) {
+-                    commandcontext.getSource().sendFailureMessage(new net.minecraft.network.chat.ChatComponentText("Failed to set the gamemode of '" + entityplayer.getName() + "'"));
++                // Paper start - handle event cancelling the change
++                org.bukkit.event.player.PlayerGameModeChangeEvent event = entityplayer.setGamemode(enumgamemode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.COMMAND, net.kyori.adventure.text.Component.text("Failed to set the gamemode of '" + entityplayer.getName() + "'", net.kyori.adventure.text.format.NamedTextColor.RED));
++                if (event != null && event.isCancelled()) {
++                    commandcontext.getSource().sendMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false);
+                     continue;
+                 }
+-                // CraftBukkit end
++                // Paper end
+                 a((CommandListenerWrapper) commandcontext.getSource(), entityplayer, enumgamemode);
+                 ++i;
+             }
+diff --git a/src/main/java/net/minecraft/server/commands/CommandGamemodeDefault.java b/src/main/java/net/minecraft/server/commands/CommandGamemodeDefault.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/commands/CommandGamemodeDefault.java
++++ b/src/main/java/net/minecraft/server/commands/CommandGamemodeDefault.java
+@@ -0,0 +0,0 @@ public class CommandGamemodeDefault {
+                 EntityPlayer entityplayer = (EntityPlayer) iterator.next();
+ 
+                 if (entityplayer.playerInteractManager.getGameMode() != enumgamemode) {
+-                    entityplayer.a(enumgamemode);
++                    // Paper start - handle event cancelling the change
++                    org.bukkit.event.player.PlayerGameModeChangeEvent event = entityplayer.setGamemode(enumgamemode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, net.kyori.adventure.text.Component.text("Failed to set the gamemode of '" + entityplayer.getName() + "'", net.kyori.adventure.text.format.NamedTextColor.RED));
++                    if (event != null && event.isCancelled()) {
++                        commandlistenerwrapper.sendMessage(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false);
++                        continue;
++                    }
++                    // Paper end
+                     ++i;
+                 }
+             }
+diff --git a/src/main/java/net/minecraft/server/level/EntityPlayer.java b/src/main/java/net/minecraft/server/level/EntityPlayer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/level/EntityPlayer.java
++++ b/src/main/java/net/minecraft/server/level/EntityPlayer.java
+@@ -0,0 +0,0 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
+         if (this.locY() > 300) this.setPositionRaw(locX(), 257, locZ()); // Paper - bring down to a saner Y level if out of world
+         if (nbttagcompound.hasKeyOfType("playerGameType", 99)) {
+             if (this.getMinecraftServer().getForceGamemode()) {
++                // Paper start - call PlayerGameModeChangeEvent on join for players that do not have the correct gamemode
++                if (this.getMinecraftServer().getGamemode() != EnumGamemode.getById(nbttagcompound.getInt("playerGameType"))) {
++                    if (new org.bukkit.event.player.PlayerGameModeChangeEvent(this.getBukkitEntity(), GameMode.getByValue(this.getMinecraftServer().getGamemode().getId()), org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, null).callEvent()) {
+                 this.playerInteractManager.a(this.getMinecraftServer().getGamemode(), EnumGamemode.NOT_SET);
++                    } else {
++                        this.playerInteractManager.a(EnumGamemode.getById(nbttagcompound.getInt("playerGameType")), nbttagcompound.hasKeyOfType("previousPlayerGameType", 3) ? EnumGamemode.getById(nbttagcompound.getInt("previousPlayerGameType")) : EnumGamemode.NOT_SET); // copied from below; if cancelled, set gamemode normally
++                    }
++                } else {
++                    this.playerInteractManager.a(EnumGamemode.getById(nbttagcompound.getInt("playerGameType")), nbttagcompound.hasKeyOfType("previousPlayerGameType", 3) ? EnumGamemode.getById(nbttagcompound.getInt("previousPlayerGameType")) : EnumGamemode.NOT_SET); // copied from below; if no change needed, set gamemode normally
++                } // Paper end
+             } else {
+                 this.playerInteractManager.a(EnumGamemode.getById(nbttagcompound.getInt("playerGameType")), nbttagcompound.hasKeyOfType("previousPlayerGameType", 3) ? EnumGamemode.getById(nbttagcompound.getInt("previousPlayerGameType")) : EnumGamemode.NOT_SET);
+             }
+@@ -0,0 +0,0 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
+ 
+     @Override
+     public void a(EnumGamemode enumgamemode) {
++        // Paper start - Add cause and nullable message to event
++        setGamemode(enumgamemode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.UNKNOWN, null);
++    }
++
++    public PlayerGameModeChangeEvent setGamemode(EnumGamemode enumgamemode, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause cause, net.kyori.adventure.text.Component message) {
++        // Paper end
+         // CraftBukkit start
+         if (enumgamemode == this.playerInteractManager.getGameMode()) {
+-            return;
++            return null; // Paper
+         }
+ 
+-        PlayerGameModeChangeEvent event = new PlayerGameModeChangeEvent(getBukkitEntity(), GameMode.getByValue(enumgamemode.getId()));
++        PlayerGameModeChangeEvent event = new PlayerGameModeChangeEvent(getBukkitEntity(), GameMode.getByValue(enumgamemode.getId()), cause, message); // Paper
+         world.getServer().getPluginManager().callEvent(event);
+         if (event.isCancelled()) {
+-            return;
++            return event; // Paper
+         }
+         // CraftBukkit end
+ 
+@@ -0,0 +0,0 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
+ 
+         this.updateAbilities();
+         this.dU();
++        return event; // Paper
+     }
+ 
+     @Override
+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 {
+ 
+                     this.player = this.minecraftServer.getPlayerList().moveToWorld(this.player, false);
+                     if (this.minecraftServer.isHardcore()) {
+-                        this.player.a(EnumGamemode.SPECTATOR);
++                        this.player.setGamemode(EnumGamemode.SPECTATOR, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.HARDCORE_DEATH, null); // Paper
+                         ((GameRules.GameRuleBoolean) this.player.getWorldServer().getGameRules().get(GameRules.SPECTATORS_GENERATE_CHUNKS)).a(false, this.minecraftServer);
+                     }
+                 }
+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 {
+             throw new IllegalArgumentException("Mode cannot be null");
+         }
+ 
+-        getHandle().a(EnumGamemode.getById(mode.getValue()));
++        getHandle().setGamemode(EnumGamemode.getById(mode.getValue()), org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.PLUGIN, null); // Paper
+     }
+ 
+     @Override