From 93dcf918f8e86986a1c2804983673ddf295b37a1 Mon Sep 17 00:00:00 2001
From: Jake Potrebic <jake.m.potrebic@gmail.com>
Date: Fri, 8 Dec 2023 15:13:02 -0800
Subject: [PATCH] more cleanup and resource pack api fixes

---
 .../api/Add-Player-Client-Options-API.patch   |  15 ++-
 patches/api/Add-PlayerKickEvent-causes.patch  |   1 +
 patches/api/Add-sendOpLevel-API.patch         |   2 +-
 patches/api/Adventure.patch                   |  87 ++++++++++--
 patches/api/Complete-resource-pack-API.patch  | 125 +++++++++++-------
 ...e-attack-cooldown-methods-for-Player.patch |  14 +-
 patches/api/Fix-upstream-javadocs.patch       |  70 ++++++++++
 patches/api/Player-elytra-boost-API.patch     |  17 +--
 patches/api/Player.setPlayerProfile-API.patch |  26 ++--
 ...-replace-OfflinePlayer-getLastPlayed.patch |  16 +--
 .../server/Add-PlayerKickEvent-causes.patch   |   2 +-
 patches/server/Adventure.patch                |  11 +-
 .../server/Complete-resource-pack-API.patch   |  62 +++------
 .../Flag-to-disable-the-channel-limit.patch   |  10 +-
 ...revious-behavior-for-setResourcePack.patch |  26 ++++
 15 files changed, 321 insertions(+), 163 deletions(-)
 create mode 100644 patches/server/Keep-previous-behavior-for-setResourcePack.patch

diff --git a/patches/api/Add-Player-Client-Options-API.patch b/patches/api/Add-Player-Client-Options-API.patch
index ebda51994d..aba1be8c1e 100644
--- a/patches/api/Add-Player-Client-Options-API.patch
+++ b/patches/api/Add-Player-Client-Options-API.patch
@@ -233,15 +233,16 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- 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
-      * Reset the cooldown counter to 0, effectively starting the cooldown period.
-      */
      void resetCooldown();
-+
+     // Paper end - attack cooldown API
+ 
++    // Paper start - client option API
 +    /**
 +     * @return the client option value of the player
 +     */
-+    @NotNull
-+    <T> T getClientOption(@NotNull com.destroystokyo.paper.ClientOption<T> option);
-     // Paper end
- 
++    <T> @NotNull T getClientOption(com.destroystokyo.paper.@NotNull ClientOption<T> option);
++    // Paper end - client option API
++
      // Spigot start
+     public class Spigot extends Entity.Spigot {
+ 
diff --git a/patches/api/Add-PlayerKickEvent-causes.patch b/patches/api/Add-PlayerKickEvent-causes.patch
index 20755a687a..512930bf4b 100644
--- a/patches/api/Add-PlayerKickEvent-causes.patch
+++ b/patches/api/Add-PlayerKickEvent-causes.patch
@@ -52,6 +52,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        this.cause = Cause.UNKNOWN;
 +    }
 +
++    @org.jetbrains.annotations.ApiStatus.Internal
 +    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;
diff --git a/patches/api/Add-sendOpLevel-API.patch b/patches/api/Add-sendOpLevel-API.patch
index 7cdb109a45..05d68a81d7 100644
--- a/patches/api/Add-sendOpLevel-API.patch
+++ b/patches/api/Add-sendOpLevel-API.patch
@@ -10,7 +10,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +++ b/src/main/java/org/bukkit/entity/Player.java
 @@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
      }
-     // Paper end
+     // Paper end - elytra boost API
  
 +    // Paper start - sendOpLevel API
 +    /**
diff --git a/patches/api/Adventure.patch b/patches/api/Adventure.patch
index ab8322fc18..8258f3e50d 100644
--- a/patches/api/Adventure.patch
+++ b/patches/api/Adventure.patch
@@ -2468,11 +2468,20 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      public void sendSignChange(@NotNull Location loc, @Nullable String[] lines, @NotNull DyeColor dyeColor, boolean hasGlowingText) throws IllegalArgumentException;
  
      /**
+@@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+      * @throws IllegalArgumentException Thrown if the URL is null.
+      * @throws IllegalArgumentException Thrown if the URL is too long.
+      * @deprecated Minecraft no longer uses textures packs. Instead you
+-     *     should use {@link #setResourcePack(String)}.
++     *     should use {@link #setResourcePack(UUID, String, byte[], net.kyori.adventure.text.Component, boolean)}.
+      */
+     @Deprecated
+     public void setTexturePack(@NotNull String url);
 @@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
       *     pack correctly.
       * </ul>
       *
-+     * @deprecated in favour of {@link #setResourcePack(String, byte[], net.kyori.adventure.text.Component)}
++     * @deprecated in favour of {@link #setResourcePack(UUID, String, byte[], net.kyori.adventure.text.Component, boolean)}
       * @param url The URL from which the client will download the resource
       *     pack. The string must contain only US-ASCII characters and should
       *     be encoded as per RFC 1738.
@@ -2480,7 +2489,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
       * @throws IllegalArgumentException Thrown if the hash is not 20 bytes
       *     long.
       */
-+    @Deprecated // Paper
++    @Deprecated // Paper - adventure
      public void setResourcePack(@NotNull String url, @Nullable byte[] hash);
  
      /**
@@ -2488,7 +2497,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
       *     pack correctly.
       * </ul>
       *
-+     * @deprecated in favour of {@link #setResourcePack(String, byte[], net.kyori.adventure.text.Component)}
++     * @deprecated in favour of {@link #setResourcePack(UUID, String, byte[], net.kyori.adventure.text.Component, boolean)}
       * @param url The URL from which the client will download the resource
       *     pack. The string must contain only US-ASCII characters and should
       *     be encoded as per RFC 1738.
@@ -2496,7 +2505,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
       * @throws IllegalArgumentException Thrown if the hash is not 20 bytes
       *     long.
       */
-+    @Deprecated // Paper
++    @Deprecated // Paper - adventure
      public void setResourcePack(@NotNull String url, @Nullable byte[] hash, @Nullable String prompt);
  
 +    // Paper start
@@ -2521,9 +2530,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     *     case this method will have no affect on them. Use the
 +     *     {@link PlayerResourcePackStatusEvent} to figure out whether or not
 +     *     the player loaded the pack!
-+     * <li>There is no concept of resetting resource packs back to default
-+     *     within Minecraft, so players will have to relog to do so or you
-+     *     have to send an empty pack.
 +     * <li>The request is sent with empty string as the hash when the hash is
 +     *     not provided. This might result in newer versions not loading the
 +     *     pack correctly.
@@ -2542,7 +2548,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @throws IllegalArgumentException Thrown if the hash is not 20 bytes
 +     *     long.
 +     */
-+    default void setResourcePack(@NotNull String url, byte @Nullable [] hash, net.kyori.adventure.text.@Nullable Component prompt) {
++    default void setResourcePack(final @NotNull String url, final byte @Nullable [] hash, final net.kyori.adventure.text.@Nullable Component prompt) {
 +        this.setResourcePack(url, hash, prompt, false);
 +    }
 +    // Paper end
@@ -2587,9 +2593,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     *     case this method will have no affect on them. Use the
 +     *     {@link PlayerResourcePackStatusEvent} to figure out whether or not
 +     *     the player loaded the pack!
-+     * <li>There is no concept of resetting resource packs back to default
-+     *     within Minecraft, so players will have to relog to do so or you
-+     *     have to send an empty pack.
 +     * <li>The request is sent with empty string as the hash when the hash is
 +     *     not provided. This might result in newer versions not loading the
 +     *     pack correctly.
@@ -2610,12 +2613,72 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @throws IllegalArgumentException Thrown if the hash is not 20 bytes
 +     *     long.
 +     */
-+    public void setResourcePack(@NotNull String url, byte @Nullable [] hash, net.kyori.adventure.text.@Nullable Component prompt, boolean force);
++    default void setResourcePack(final @NotNull String url, final byte @Nullable [] hash, final net.kyori.adventure.text.@Nullable Component prompt, final boolean force) {
++        this.setResourcePack(UUID.nameUUIDFromBytes(url.getBytes(java.nio.charset.StandardCharsets.UTF_8)), url, hash, prompt, force);
++    }
 +    // Paper end
 +
      /**
       * Request that the player's client download and switch resource packs.
       * <p>
+@@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+      *     length restriction is an implementation specific arbitrary value.
+      * @throws IllegalArgumentException Thrown if the hash is not 20 bytes
+      *     long.
++     * @deprecated use {@link #setResourcePack(UUID, String, byte[], net.kyori.adventure.text.Component, boolean)} )}
+      */
++    @Deprecated // Paper - adventure
+     public void setResourcePack(@NotNull UUID id, @NotNull String url, @Nullable byte[] hash, @Nullable String prompt, boolean force);
+ 
++    // Paper start
++    /**
++     * Request that the player's client download and switch resource packs.
++     * <p>
++     * The player's client will download the new resource pack asynchronously
++     * in the background, and will automatically switch to it once the
++     * download is complete. If the client has downloaded and cached a
++     * resource pack with the same hash in the past it will not download but
++     * directly apply the cached pack. If the hash is null and the client has
++     * downloaded and cached the same resource pack in the past, it will
++     * perform a file size check against the response content to determine if
++     * the resource pack has changed and needs to be downloaded again. When
++     * this request is sent for the very first time from a given server, the
++     * client will first display a confirmation GUI to the player before
++     * proceeding with the download.
++     * <p>
++     * Notes:
++     * <ul>
++     * <li>Players can disable server resources on their client, in which
++     *     case this method will have no affect on them. Use the
++     *     {@link PlayerResourcePackStatusEvent} to figure out whether or not
++     *     the player loaded the pack!
++     * <li>The request is sent with empty string as the hash when the hash is
++     *     not provided. This might result in newer versions not loading the
++     *     pack correctly.
++     * </ul>
++     *
++     * @param uuid Unique resource pack ID.
++     * @param url The URL from which the client will download the resource
++     *     pack. The string must contain only US-ASCII characters and should
++     *     be encoded as per RFC 1738.
++     * @param hash The sha1 hash sum of the resource pack file which is used
++     *     to apply a cached version of the pack directly without downloading
++     *     if it is available. Hast to be 20 bytes long!
++     * @param prompt The optional custom prompt message to be shown to client.
++     * @param force If true, the client will be disconnected from the server
++     *     when it declines to use the resource pack.
++     * @throws IllegalArgumentException Thrown if the URL is null.
++     * @throws IllegalArgumentException Thrown if the URL is too long. The
++     *     length restriction is an implementation specific arbitrary value.
++     * @throws IllegalArgumentException Thrown if the hash is not 20 bytes
++     *     long.
++     */
++    void setResourcePack(@NotNull UUID uuid, @NotNull String url, byte @Nullable [] hash, net.kyori.adventure.text.@Nullable Component prompt, boolean force);
++    // Paper end
++
+     /**
+      * Gets the Scoreboard displayed to this player
+      *
 @@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
       *
       * @param title Title text
diff --git a/patches/api/Complete-resource-pack-API.patch b/patches/api/Complete-resource-pack-API.patch
index ae0faf2e0b..e90fd0bcf7 100644
--- a/patches/api/Complete-resource-pack-API.patch
+++ b/patches/api/Complete-resource-pack-API.patch
@@ -19,10 +19,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
  
      /**
 @@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
-     default net.kyori.adventure.text.event.HoverEvent<net.kyori.adventure.text.event.HoverEvent.ShowEntity> asHoverEvent(final @NotNull java.util.function.UnaryOperator<net.kyori.adventure.text.event.HoverEvent.ShowEntity> op) {
-         return net.kyori.adventure.text.event.HoverEvent.showEntity(op.apply(net.kyori.adventure.text.event.HoverEvent.ShowEntity.of(this.getType().getKey(), this.getUniqueId(), this.displayName())));
-     }
-+
+     void setResourcePack(@NotNull UUID uuid, @NotNull String url, byte @Nullable [] hash, net.kyori.adventure.text.@Nullable Component prompt, boolean force);
+     // Paper end
+ 
++    // Paper start - more resource pack API
 +    /**
 +     * Request that the player's client download and switch resource packs.
 +     * <p>
@@ -39,8 +39,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * <ul>
 +     * <li>Players can disable server resources on their client, in which
 +     *     case this method will have no affect on them.
-+     * <li>There is no concept of resetting resource packs back to default
-+     *     within Minecraft, so players will have to relog to do so.
 +     * </ul>
 +     *
 +     * @param url The URL from which the client will download the resource
@@ -52,7 +50,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @throws IllegalArgumentException Thrown if the URL is too long. The
 +     *     length restriction is an implementation specific arbitrary value.
 +     */
-+    void setResourcePack(@NotNull String url, @NotNull String hash);
++    default void setResourcePack(final @NotNull String url, final @NotNull String hash) {
++        this.setResourcePack(url, hash, false);
++    }
 +
 +    /**
 +     * Request that the player's client download and switch resource packs.
@@ -70,8 +70,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * <ul>
 +     * <li>Players can disable server resources on their client, in which
 +     *     case this method will have no affect on them.
-+     * <li>There is no concept of resetting resource packs back to default
-+     *     within Minecraft, so players will have to relog to do so.
 +     * </ul>
 +     *
 +     * @param url The URL from which the client will download the resource
@@ -84,7 +82,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @throws IllegalArgumentException Thrown if the URL is too long. The
 +     *     length restriction is an implementation specific arbitrary value.
 +     */
-+    void setResourcePack(@NotNull String url, @NotNull String hash, boolean required);
++    default void setResourcePack(final @NotNull String url, final @NotNull String hash, final boolean required) {
++        this.setResourcePack(url, hash, required, null);
++    }
 +
 +    /**
 +     * Request that the player's client download and switch resource packs.
@@ -102,8 +102,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * <ul>
 +     * <li>Players can disable server resources on their client, in which
 +     *     case this method will have no affect on them.
-+     * <li>There is no concept of resetting resource packs back to default
-+     *     within Minecraft, so players will have to relog to do so.
 +     * </ul>
 +     *
 +     * @param url The URL from which the client will download the resource
@@ -117,65 +115,90 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @throws IllegalArgumentException Thrown if the URL is too long. The
 +     *     length restriction is an implementation specific arbitrary value.
 +     */
-+    void setResourcePack(@NotNull String url, @NotNull String hash, boolean required, @Nullable net.kyori.adventure.text.Component resourcePackPrompt);
-+    /**
-+     * @return the most recent resource pack status received from the player,
-+     *         or null if no status has ever been received from this player.
-+     */
-+    @Nullable
-+    org.bukkit.event.player.PlayerResourcePackStatusEvent.Status getResourcePackStatus();
++    default void setResourcePack(final @NotNull String url, final @NotNull String hash, final boolean required, final net.kyori.adventure.text.@Nullable Component resourcePackPrompt) {
++        this.setResourcePack(UUID.nameUUIDFromBytes(url.getBytes(java.nio.charset.StandardCharsets.UTF_8)), url, hash, resourcePackPrompt, required);
++    }
 +
 +    /**
-+     * @return the most recent resource pack hash received from the player,
-+     *         or null if no hash has ever been received from this player.
++     * Request that the player's client download and switch resource packs.
++     * <p>
++     * The player's client will download the new resource pack asynchronously
++     * in the background, and will automatically switch to it once the
++     * download is complete. If the client has downloaded and cached the same
++     * resource pack in the past, it will perform a quick timestamp check
++     * over the network to determine if the resource pack has changed and
++     * needs to be downloaded again. When this request is sent for the very
++     * first time from a given server, the client will first display a
++     * confirmation GUI to the player before proceeding with the download.
++     * <p>
++     * Notes:
++     * <ul>
++     * <li>Players can disable server resources on their client, in which
++     *     case this method will have no affect on them.
++     * </ul>
 +     *
++     * @param uuid Unique resource pack ID.
++     * @param url The URL from which the client will download the resource
++     *     pack. The string must contain only US-ASCII characters and should
++     *     be encoded as per RFC 1738.
++     * @param hash A 40 character hexadecimal and lowercase SHA-1 digest of
++     *     the resource pack file.
++     * @param resourcePackPrompt A Prompt to be displayed in the client request
++     * @param required Marks if the resource pack should be required by the client
++     * @throws IllegalArgumentException Thrown if the URL is null.
++     * @throws IllegalArgumentException Thrown if the URL is too long. The
++     *     length restriction is an implementation specific arbitrary value.
++     */
++    void setResourcePack(@NotNull UUID uuid, @NotNull String url, @NotNull String hash, net.kyori.adventure.text.@Nullable Component resourcePackPrompt, boolean required);
++
++    /**
++     * Gets the most recent resource pack status from the player.
++     *
++     * @return the most recent status or null
++     */
++    org.bukkit.event.player.PlayerResourcePackStatusEvent.@Nullable Status getResourcePackStatus();
++
++    /**
++     * Gets the most recent pack hash from the player.
++     *
++     * @return the most recent hash or null
 +     * @deprecated This is no longer sent from the client and will always be null
 +     */
-+    @Nullable
-+    @Deprecated
-+    String getResourcePackHash();
++    @Deprecated(forRemoval = true)
++    @org.jetbrains.annotations.Contract("-> null")
++    default @Nullable String getResourcePackHash() {
++        return null;
++    }
 +
 +    /**
-+     * @return true if the last resource pack status received from this player
-+     *         was {@link org.bukkit.event.player.PlayerResourcePackStatusEvent.Status#SUCCESSFULLY_LOADED}
++     * Gets if the last resource pack status from the player
++     * was {@link org.bukkit.event.player.PlayerResourcePackStatusEvent.Status#SUCCESSFULLY_LOADED}.
++     *
++     * @return true if last status was successfully loaded
 +     */
-+    boolean hasResourcePack();
-     // Paper end
- 
-     // Spigot start
++    default boolean hasResourcePack() {
++        return this.getResourcePackStatus() == org.bukkit.event.player.PlayerResourcePackStatusEvent.Status.SUCCESSFULLY_LOADED;
++    }
++    // Paper end - more resource pack API
++
+     /**
+      * Gets the Scoreboard displayed to this player
+      *
 diff --git a/src/main/java/org/bukkit/event/player/PlayerResourcePackStatusEvent.java b/src/main/java/org/bukkit/event/player/PlayerResourcePackStatusEvent.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/org/bukkit/event/player/PlayerResourcePackStatusEvent.java
 +++ b/src/main/java/org/bukkit/event/player/PlayerResourcePackStatusEvent.java
 @@ -0,0 +0,0 @@ public class PlayerResourcePackStatusEvent extends PlayerEvent {
- 
-     private static final HandlerList handlers = new HandlerList();
-     private final UUID id;
-+    @Deprecated
-+    private final String hash; // Paper
-     private final Status status;
- 
-     public PlayerResourcePackStatusEvent(@NotNull final Player who, @NotNull UUID id, @NotNull Status resourcePackStatus) {
-         super(who);
-         this.id = id;
-+        this.hash = null; // Paper
          this.status = resourcePackStatus;
      }
  
-+    @Deprecated // Paper
-+    public PlayerResourcePackStatusEvent(@NotNull final Player who, @NotNull UUID id, Status resourcePackStatus, String hash) {
-+        super(who);
-+        this.id = id;
-+        this.hash = hash; // Paper
-+        this.status = resourcePackStatus;
-+    }
-+
++    // Paper start - add hash (not used anymore)
 +    /**
 +     * @deprecated Hash does not seem to ever be set
 +     */
-+    @Deprecated
++    @Deprecated(forRemoval = true)
 +    public String getHash() {
-+        return this.hash;
++        return null;
 +    }
 +    // Paper end
 +
diff --git a/patches/api/Expose-attack-cooldown-methods-for-Player.patch b/patches/api/Expose-attack-cooldown-methods-for-Player.patch
index de20390c3f..77e7203f00 100644
--- a/patches/api/Expose-attack-cooldown-methods-for-Player.patch
+++ b/patches/api/Expose-attack-cooldown-methods-for-Player.patch
@@ -9,10 +9,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- 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 profile The new profile to use
-      */
-     void setPlayerProfile(@NotNull com.destroystokyo.paper.profile.PlayerProfile profile);
-+
+     void setPlayerProfile(com.destroystokyo.paper.profile.@NotNull PlayerProfile profile);
+     // Paper end - Player Profile API
+ 
++    // Paper start - attack cooldown API
 +    /**
 +     * Returns the amount of ticks the current cooldown lasts
 +     *
@@ -32,6 +32,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * Reset the cooldown counter to 0, effectively starting the cooldown period.
 +     */
 +    void resetCooldown();
-     // Paper end
- 
++    // Paper end - attack cooldown API
++
      // Spigot start
+     public class Spigot extends Entity.Spigot {
+ 
diff --git a/patches/api/Fix-upstream-javadocs.patch b/patches/api/Fix-upstream-javadocs.patch
index 8e7703b345..409ad7ff86 100644
--- a/patches/api/Fix-upstream-javadocs.patch
+++ b/patches/api/Fix-upstream-javadocs.patch
@@ -445,6 +445,76 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
       * remain hidden until the other plugin calls this method too.
       *
       * @param plugin Plugin that wants to show the entity
+@@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+      *     case this method will have no affect on them. Use the
+      *     {@link PlayerResourcePackStatusEvent} to figure out whether or not
+      *     the player loaded the pack!
+-     * <li>There is no concept of resetting texture packs back to default
+-     *     within Minecraft, so players will have to relog to do so or you
+-     *     have to send an empty pack.
+      * <li>The request is send with "null" as the hash. This might result
+      *     in newer versions not loading the pack correctly.
+      * </ul>
+@@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+      *     case this method will have no affect on them. Use the
+      *     {@link PlayerResourcePackStatusEvent} to figure out whether or not
+      *     the player loaded the pack!
+-     * <li>There is no concept of resetting resource packs back to default
+-     *     within Minecraft, so players will have to relog to do so or you
+-     *     have to send an empty pack.
+      * <li>The request is send with empty string as the hash. This might result
+      *     in newer versions not loading the pack correctly.
+      * </ul>
+@@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+      *     case this method will have no affect on them. Use the
+      *     {@link PlayerResourcePackStatusEvent} to figure out whether or not
+      *     the player loaded the pack!
+-     * <li>There is no concept of resetting resource packs back to default
+-     *     within Minecraft, so players will have to relog to do so or you
+-     *     have to send an empty pack.
+      * <li>The request is sent with empty string as the hash when the hash is
+      *     not provided. This might result in newer versions not loading the
+      *     pack correctly.
+@@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+      *     case this method will have no affect on them. Use the
+      *     {@link PlayerResourcePackStatusEvent} to figure out whether or not
+      *     the player loaded the pack!
+-     * <li>There is no concept of resetting resource packs back to default
+-     *     within Minecraft, so players will have to relog to do so or you
+-     *     have to send an empty pack.
+      * <li>The request is sent with empty string as the hash when the hash is
+      *     not provided. This might result in newer versions not loading the
+      *     pack correctly.
+@@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+      *     case this method will have no affect on them. Use the
+      *     {@link PlayerResourcePackStatusEvent} to figure out whether or not
+      *     the player loaded the pack!
+-     * <li>There is no concept of resetting resource packs back to default
+-     *     within Minecraft, so players will have to relog to do so or you
+-     *     have to send an empty pack.
+      * <li>The request is sent with empty string as the hash when the hash is
+      *     not provided. This might result in newer versions not loading the
+      *     pack correctly.
+@@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+      *     case this method will have no affect on them. Use the
+      *     {@link PlayerResourcePackStatusEvent} to figure out whether or not
+      *     the player loaded the pack!
+-     * <li>There is no concept of resetting resource packs back to default
+-     *     within Minecraft, so players will have to relog to do so or you
+-     *     have to send an empty pack.
+      * <li>The request is sent with empty string as the hash when the hash is
+      *     not provided. This might result in newer versions not loading the
+      *     pack correctly.
+@@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+      *     case this method will have no affect on them. Use the
+      *     {@link PlayerResourcePackStatusEvent} to figure out whether or not
+      *     the player loaded the pack!
+-     * <li>There is no concept of resetting resource packs back to default
+-     *     within Minecraft, so players will have to relog to do so or you
+-     *     have to send an empty pack.
+      * <li>The request is sent with empty string as the hash when the hash is
+      *     not provided. This might result in newer versions not loading the
+      *     pack correctly.
 diff --git a/src/main/java/org/bukkit/entity/Slime.java b/src/main/java/org/bukkit/entity/Slime.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/org/bukkit/entity/Slime.java
diff --git a/patches/api/Player-elytra-boost-API.patch b/patches/api/Player-elytra-boost-API.patch
index 116df06bf1..595567cd76 100644
--- a/patches/api/Player-elytra-boost-API.patch
+++ b/patches/api/Player-elytra-boost-API.patch
@@ -9,10 +9,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- 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
-      */
-     @NotNull
-     <T> T getClientOption(@NotNull com.destroystokyo.paper.ClientOption<T> option);
-+
+     <T> @NotNull T getClientOption(com.destroystokyo.paper.@NotNull ClientOption<T> option);
+     // Paper end - client option API
+ 
++    // Paper start - elytra boost API
 +    /**
 +     * Boost a Player that's {@link #isGliding()} using a {@link Firework}.
 +     * If the creation of the entity is cancelled, no boosting is done.
@@ -25,11 +25,12 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +     * @deprecated use {@link HumanEntity#fireworkBoost(ItemStack)} instead. Note that this method <b>does not</b>
 +     * check if the player is gliding or not.
 +     */
-+    @Nullable
-+    default Firework boostElytra(@NotNull ItemStack firework) {
++    default @Nullable Firework boostElytra(final @NotNull ItemStack firework) {
 +        com.google.common.base.Preconditions.checkState(this.isGliding(), "Player must be gliding");
 +        return this.fireworkBoost(firework);
 +    }
-     // Paper end
- 
++    // Paper end - elytra boost API
++
      // Spigot start
+     public class Spigot extends Entity.Spigot {
+ 
diff --git a/patches/api/Player.setPlayerProfile-API.patch b/patches/api/Player.setPlayerProfile-API.patch
index 6806c454cc..eeac20201e 100644
--- a/patches/api/Player.setPlayerProfile-API.patch
+++ b/patches/api/Player.setPlayerProfile-API.patch
@@ -97,30 +97,32 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- 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
-      *         was {@link org.bukkit.event.player.PlayerResourcePackStatusEvent.Status#SUCCESSFULLY_LOADED}
-      */
-     boolean hasResourcePack();
-+
+     }
+     // Paper end
+ 
++    // Paper start - Player Profile API
 +    /**
 +     * Gets a copy of this players profile
++     *
 +     * @return The players profile object
 +     */
-+    @NotNull
-+    com.destroystokyo.paper.profile.PlayerProfile getPlayerProfile();
++    com.destroystokyo.paper.profile.@NotNull PlayerProfile getPlayerProfile();
 +
 +    /**
 +     * Changes the PlayerProfile for this player. This will cause this player
-+     * to be reregistered to all clients that can currently see this player.
-+     *
++     * to be re-registered to all clients that can currently see this player.
++     * <p>
 +     * After executing this method, the player {@link java.util.UUID} won't
-+     * be swapped, only their name and gameprofile properties.
++     * be swapped, only their name and profile properties.
 +     *
 +     * @param profile The new profile to use
 +     */
-+    void setPlayerProfile(@NotNull com.destroystokyo.paper.profile.PlayerProfile profile);
-     // Paper end
- 
++    void setPlayerProfile(com.destroystokyo.paper.profile.@NotNull PlayerProfile profile);
++    // Paper end - Player Profile API
++
      // Spigot start
+     public class Spigot extends Entity.Spigot {
+ 
 diff --git a/src/main/java/org/bukkit/profile/PlayerProfile.java b/src/main/java/org/bukkit/profile/PlayerProfile.java
 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
 --- a/src/main/java/org/bukkit/profile/PlayerProfile.java
diff --git a/patches/server/Add-APIs-to-replace-OfflinePlayer-getLastPlayed.patch b/patches/server/Add-APIs-to-replace-OfflinePlayer-getLastPlayed.patch
index f4e24d8b55..7e31065ba7 100644
--- a/patches/server/Add-APIs-to-replace-OfflinePlayer-getLastPlayed.patch
+++ b/patches/server/Add-APIs-to-replace-OfflinePlayer-getLastPlayed.patch
@@ -110,28 +110,28 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- 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 {
-     private org.bukkit.event.player.PlayerResourcePackStatusEvent.Status resourcePackStatus;
-     private String resourcePackHash;
+     private BorderChangeListener clientWorldBorderListener = this.createWorldBorderListener();
+     public org.bukkit.event.player.PlayerResourcePackStatusEvent.Status resourcePackStatus; // Paper - more resource pack API
      private static final boolean DISABLE_CHANNEL_LIMIT = System.getProperty("paper.disableChannelLimit") != null; // Paper - add a flag to disable the channel limit
-+    private long lastSaveTime;
-     // Paper end
++    private long lastSaveTime; // Paper - getLastPlayed replacement API
  
      public CraftPlayer(CraftServer server, ServerPlayer entity) {
+         super(server, entity);
 @@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
          this.firstPlayed = firstPlayed;
      }
  
-+    // Paper start
++    // Paper start - getLastPlayed replacement API
 +    @Override
 +    public long getLastLogin() {
-+        return getHandle().loginTime;
++        return this.getHandle().loginTime;
 +    }
 +
 +    @Override
 +    public long getLastSeen() {
-+        return isOnline() ? System.currentTimeMillis() : this.lastSaveTime;
++        return this.isOnline() ? System.currentTimeMillis() : this.lastSaveTime;
 +    }
-+    // Paper end
++    // Paper end - getLastPlayed replacement API
 +
      public void readExtraData(CompoundTag nbttagcompound) {
          this.hasPlayedBefore = true;
diff --git a/patches/server/Add-PlayerKickEvent-causes.patch b/patches/server/Add-PlayerKickEvent-causes.patch
index 4ce72c1d71..150dae0a44 100644
--- a/patches/server/Add-PlayerKickEvent-causes.patch
+++ b/patches/server/Add-PlayerKickEvent-causes.patch
@@ -13,7 +13,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
              SignedMessageLink signedMessageLink = this.advanceLink();
              if (signedMessageLink == null) {
 -                throw new SignedMessageChain.DecodeException(Component.translatable("chat.disabled.chain_broken"), false);
-+                throw new SignedMessageChain.DecodeException(Component.translatable("chat.disabled.chain_broken"), false); // Paper - TODO 1.20.3 - new kick cause?
++                throw new SignedMessageChain.DecodeException(Component.translatable("chat.disabled.chain_broken"), false); // Paper - diff on change (if disconnects, need a new kick event cause)
              } else if (playerPublicKey.data().hasExpired()) {
 -                throw new SignedMessageChain.DecodeException(Component.translatable("chat.disabled.expiredProfileKey"), false);
 +                throw new SignedMessageChain.DecodeException(Component.translatable("chat.disabled.expiredProfileKey", org.bukkit.event.player.PlayerKickEvent.Cause.EXPIRED_PROFILE_PUBLIC_KEY), false); // Paper - kick event causes
diff --git a/patches/server/Adventure.patch b/patches/server/Adventure.patch
index 6910a8ada4..0a0a4a52c2 100644
--- a/patches/server/Adventure.patch
+++ b/patches/server/Adventure.patch
@@ -3755,10 +3755,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
          }
      }
  
-+    // Paper start
-+    // TODO UUID API
++    // Paper start - adventure
 +    @Override
-+    public void setResourcePack(String url, byte[] hashBytes, net.kyori.adventure.text.Component prompt, boolean force) {
++    public void setResourcePack(final UUID uuid, final String url, final byte[] hashBytes, final net.kyori.adventure.text.Component prompt, final boolean force) {
++        Preconditions.checkArgument(uuid != null, "Resource pack UUID cannot be null");
 +        Preconditions.checkArgument(url != null, "Resource pack URL cannot be null");
 +        final String hash;
 +        if (hashBytes != null) {
@@ -3767,9 +3767,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        } else {
 +            hash = "";
 +        }
-+        this.getHandle().connection.send(new ClientboundResourcePackPushPacket(UUID.randomUUID(), url, hash, force, io.papermc.paper.adventure.PaperAdventure.asVanilla(prompt)));
++        this.getHandle().connection.send(new net.minecraft.network.protocol.common.ClientboundResourcePackPopPacket(Optional.empty()));
++        this.getHandle().connection.send(new ClientboundResourcePackPushPacket(uuid, url, hash, force, io.papermc.paper.adventure.PaperAdventure.asVanilla(prompt)));
 +    }
-+    // Paper end
++    // Paper end - adventure
 +
      public void addChannel(String channel) {
          Preconditions.checkState(this.channels.size() < 128, "Cannot register channel '%s'. Too many channels registered!", channel);
diff --git a/patches/server/Complete-resource-pack-API.patch b/patches/server/Complete-resource-pack-API.patch
index 98a2576f6b..24aadbae7b 100644
--- a/patches/server/Complete-resource-pack-API.patch
+++ b/patches/server/Complete-resource-pack-API.patch
@@ -15,7 +15,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 -        this.cserver.getPluginManager().callEvent(new PlayerResourcePackStatusEvent(this.getCraftPlayer(), packet.id(), PlayerResourcePackStatusEvent.Status.values()[packet.action().ordinal()])); // CraftBukkit
 +        // Paper start
 +        PlayerResourcePackStatusEvent.Status packStatus = PlayerResourcePackStatusEvent.Status.values()[packet.action().ordinal()];
-+        player.getBukkitEntity().setResourcePackStatus(packStatus);
++        player.getBukkitEntity().resourcePackStatus = packStatus;
 +        this.cserver.getPluginManager().callEvent(new PlayerResourcePackStatusEvent(this.getCraftPlayer(), packet.id(), packStatus)); // CraftBukkit
 +        // Paper end
  
@@ -25,48 +25,28 @@ diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/
 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 @@ import net.minecraft.network.chat.Component;
- import net.minecraft.network.chat.PlayerChatMessage;
- import net.minecraft.network.protocol.Packet;
- import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket;
-+import net.minecraft.network.protocol.common.ClientboundResourcePackPopPacket;
- import net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket;
- import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
- import net.minecraft.network.protocol.game.ClientboundBlockDestructionPacket;
 @@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
      private double healthScale = 20;
      private CraftWorldBorder clientWorldBorder = null;
      private BorderChangeListener clientWorldBorderListener = this.createWorldBorderListener();
-+    // Paper start
-+    private org.bukkit.event.player.PlayerResourcePackStatusEvent.Status resourcePackStatus;
-+    private String resourcePackHash;
-+    // Paper end
++    public org.bukkit.event.player.PlayerResourcePackStatusEvent.Status resourcePackStatus; // Paper - more resource pack API
  
      public CraftPlayer(CraftServer server, ServerPlayer entity) {
          super(server, entity);
 @@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
-     public boolean getAffectsSpawning() {
-         return this.getHandle().affectsSpawning;
      }
-+
+     // Paper end - adventure
+ 
++    // Paper start - more resource pack API
 +    @Override
-+    public void setResourcePack(@NotNull String url, @NotNull String hash) {
-+        this.setResourcePack(url, hash, false, null);
-+    }
-+
-+    @Override
-+    public void setResourcePack(@NotNull String url, @NotNull String hash, boolean required) {
-+        this.setResourcePack(url, hash, required, null);
-+    }
-+
-+    @Override
-+    public void setResourcePack(@NotNull String url, @NotNull String hash, boolean required, net.kyori.adventure.text.Component resourcePackPrompt) {
++    public void setResourcePack(@NotNull UUID uuid, @NotNull String url, @NotNull String hash, net.kyori.adventure.text.Component resourcePackPrompt, boolean required) {
++        Preconditions.checkArgument(uuid != null, "Resource pack UUID cannot be null");
 +        Preconditions.checkArgument(url != null, "Resource pack URL cannot be null");
 +        Preconditions.checkArgument(hash != null, "Hash cannot be null");
-+        net.minecraft.network.chat.Component promptComponent = resourcePackPrompt != null ?
-+                            io.papermc.paper.adventure.PaperAdventure.asVanilla(resourcePackPrompt) :
-+                           null;
-+        this.getHandle().connection.send(new ClientboundResourcePackPopPacket(Optional.empty()));
++        final net.minecraft.network.chat.Component promptComponent = resourcePackPrompt != null ?
++            io.papermc.paper.adventure.PaperAdventure.asVanilla(resourcePackPrompt) :
++            null;
++        this.getHandle().connection.send(new net.minecraft.network.protocol.common.ClientboundResourcePackPopPacket(Optional.empty()));
 +        this.getHandle().connection.send(new ClientboundResourcePackPushPacket(UUID.randomUUID(), url, hash, required, promptComponent));
 +    }
 +
@@ -74,20 +54,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    public org.bukkit.event.player.PlayerResourcePackStatusEvent.Status getResourcePackStatus() {
 +        return this.resourcePackStatus;
 +    }
++    // Paper end - more resource pack API
 +
-+    @Override
-+    public String getResourcePackHash() {
-+        return this.resourcePackHash;
-+    }
-+
-+    @Override
-+    public boolean hasResourcePack() {
-+        return this.resourcePackStatus == org.bukkit.event.player.PlayerResourcePackStatusEvent.Status.SUCCESSFULLY_LOADED;
-+    }
-+
-+    public void setResourcePackStatus(org.bukkit.event.player.PlayerResourcePackStatusEvent.Status status) {
-+        this.resourcePackStatus = status;
-+    }
-     // Paper end
- 
-     @Override
+     public void addChannel(String channel) {
+         Preconditions.checkState(this.channels.size() < 128, "Cannot register channel '%s'. Too many channels registered!", channel);
+         channel = StandardMessenger.validateAndCorrectChannel(channel);
diff --git a/patches/server/Flag-to-disable-the-channel-limit.patch b/patches/server/Flag-to-disable-the-channel-limit.patch
index e8cbe56c1b..09745a21db 100644
--- a/patches/server/Flag-to-disable-the-channel-limit.patch
+++ b/patches/server/Flag-to-disable-the-channel-limit.patch
@@ -13,15 +13,15 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- 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 {
-     // Paper start
-     private org.bukkit.event.player.PlayerResourcePackStatusEvent.Status resourcePackStatus;
-     private String resourcePackHash;
+     private CraftWorldBorder clientWorldBorder = null;
+     private BorderChangeListener clientWorldBorderListener = this.createWorldBorderListener();
+     public org.bukkit.event.player.PlayerResourcePackStatusEvent.Status resourcePackStatus; // Paper - more resource pack API
 +    private static final boolean DISABLE_CHANNEL_LIMIT = System.getProperty("paper.disableChannelLimit") != null; // Paper - add a flag to disable the channel limit
-     // Paper end
  
      public CraftPlayer(CraftServer server, ServerPlayer entity) {
+         super(server, entity);
 @@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
-     // Paper end
+     // Paper end - more resource pack API
  
      public void addChannel(String channel) {
 -        Preconditions.checkState(this.channels.size() < 128, "Cannot register channel '%s'. Too many channels registered!", channel);
diff --git a/patches/server/Keep-previous-behavior-for-setResourcePack.patch b/patches/server/Keep-previous-behavior-for-setResourcePack.patch
new file mode 100644
index 0000000000..ac725f065f
--- /dev/null
+++ b/patches/server/Keep-previous-behavior-for-setResourcePack.patch
@@ -0,0 +1,26 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Jake Potrebic <jake.m.potrebic@gmail.com>
+Date: Fri, 8 Dec 2023 15:06:16 -0800
+Subject: [PATCH] Keep previous behavior for setResourcePack
+
+Before multiple packs were allowed, setResourcePack
+resulted in the client's existing server pack being
+replaced. To keep this behavior, we will remove all
+packs before sending the new pack. Other API exists
+for adding a new pack to the existing packs on a client.
+
+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 {
+         if (hash != null) {
+             Preconditions.checkArgument(hash.length == 20, "Resource pack hash should be 20 bytes long but was %s", hash.length);
+ 
++            this.getHandle().connection.send(new net.minecraft.network.protocol.common.ClientboundResourcePackPopPacket(Optional.empty())); // Paper - keep previous behavior of clearing packs
+             this.getHandle().connection.send(new ClientboundResourcePackPushPacket(id, url, BaseEncoding.base16().lowerCase().encode(hash), force, CraftChatMessage.fromStringOrNull(prompt, true)));
+         } else {
++            this.getHandle().connection.send(new net.minecraft.network.protocol.common.ClientboundResourcePackPopPacket(Optional.empty())); // Paper - keep previous behavior of clearing packs
+             this.getHandle().connection.send(new ClientboundResourcePackPushPacket(id, url, "", force, CraftChatMessage.fromStringOrNull(prompt, true)));
+         }
+     }